Compare commits
43 commits
feature-tu
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6120e71416 | |||
| 6c564f1638 | |||
| acb1fed1f2 | |||
| 12bb949834 | |||
| 8cb1cb143c | |||
| cc6f29b8f4 | |||
| 894d0d56c0 | |||
| d9fb813be7 | |||
| 1d9055628a | |||
| 1def117e44 | |||
| 17698ce109 | |||
| 580e5f50d9 | |||
| db2c573642 | |||
| 6a643ae790 | |||
| 4644f21f07 | |||
| 49ae714955 | |||
| ae0beda299 | |||
| bfbaf86aed | |||
| 88c54b7aef | |||
| 6aaea215e1 | |||
| 8869b3cb6d | |||
| b4c664fe12 | |||
| 40387bdf6b | |||
| 31e3237b06 | |||
| 0f54c35be6 | |||
| 22c40779ef | |||
| c6bc0e9935 | |||
| 6d23069103 | |||
| 2dbe3e61ed | |||
| bfe4dd6033 | |||
| c694258bd9 | |||
| 3279022829 | |||
| 657b3a2e63 | |||
| d7dd9aea4a | |||
| 234f1a60cf | |||
| ad77c897e3 | |||
| b55ec8532c | |||
| da2681245b | |||
| e079cfa73a | |||
| db725f987d | |||
| 1801f1beb9 | |||
| 477c0d6205 | |||
| bbd02f840e |
216 changed files with 18314 additions and 5749 deletions
|
|
@ -80,7 +80,7 @@ CheckOptions:
|
|||
- key: readability-identifier-naming.PointerParameterPrefix
|
||||
value: "p_" # uint8_t *p_buffer
|
||||
- key: readability-function-size.LineThreshold
|
||||
value: '60'
|
||||
value: '90'
|
||||
- key: readability-function-size.StatementThreshold
|
||||
value: '30'
|
||||
- key: readability-magic-numbers.IgnoredIntegerValues
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ FLASHLOADER_VID=15a2
|
|||
FLASHLOADER_PID=0073
|
||||
SERVICE_CDC_VID=1996
|
||||
SERVICE_CDC_PID=00ad
|
||||
|
||||
SERVICE_M5_VID=303a
|
||||
SERVICE_M5_PID=4001
|
||||
# --- Paths ---
|
||||
# BUILD_DIR и TOOLS_DIR задаются абсолютно в корневом justfile
|
||||
# через justfile_directory(), здесь можно переопределить если нужно
|
||||
|
|
@ -61,4 +62,4 @@ HIL_USB_CDC_BAUD=115200
|
|||
HIL_USB_CDC_TIMEOUT=5.0
|
||||
|
||||
# Тип сборки firmware_test для TUI (Debug | Release)
|
||||
FIRMWARE_BUILD_TYPE=Release
|
||||
FIRMWARE_BUILD_TYPE=Debug
|
||||
338
.github/workflows/release.yml
vendored
Normal file
338
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
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,3 +77,4 @@ tools/host/.venv-host/
|
|||
tools/host/.venv-host-win/
|
||||
.zed/
|
||||
project_tree.txt
|
||||
tools/service_tui/dist/
|
||||
|
|
|
|||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -38,7 +38,7 @@
|
|||
"interface": "swd",
|
||||
"loadFiles": [],
|
||||
"runToEntryPoint": "main",
|
||||
"preLaunchTask": "build-and-rtt:firmware-test-debug",
|
||||
"preLaunchTask": "build:bootloader-debug",
|
||||
},
|
||||
// =============================================================
|
||||
// firmware/tft_app — FreeRTOS task view
|
||||
|
|
|
|||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"[python]": {
|
||||
"[python]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
}
|
||||
|
|
|
|||
322
CHANGELOG.md
322
CHANGELOG.md
|
|
@ -52,11 +52,327 @@
|
|||
|
||||
### Что отслеживать
|
||||
|
||||
- Проверять, остаётся ли `.github/workflows/ci.yml` только сборочным workflow через `just ci::build` или начинает запускать host-тесты.
|
||||
- ~~Проверять, остаётся ли `.github/workflows/ci.yml` только сборочным workflow~~ — решено: job `test` уже запускает host-тесты (`just ci::test` → `just build::test-host-release`).
|
||||
- Отслеживать появление self-hosted runner, регулярных аппаратных прогонов или отчётов по HIL.
|
||||
- Проверять новые firmware-test модули в `firmware/test/src/tests/` и синхронные обновления протокола/документации.
|
||||
- Следить за развитием BSP: MQS, RGB, bootloader или `tft_app`.
|
||||
- Отслеживать локальные патчи поверх vendor SDK, которые нужно вести отдельным patch log.
|
||||
- Следить за развитием BSP: RGB (частично закрыто display-тестом); `tft_app` — директория всё ещё не заведена (bootloader — реализован, Фазы 0–6, см. запись ниже).
|
||||
- Отслеживать локальные патчи поверх vendor SDK, которые нужно вести отдельным patch log (пример — точечный патч `fault_injection_hardening.c` под `#if defined(__arm__)`, bootloader Фаза 3, см. запись ниже).
|
||||
- ~~Отслеживать мерж ветки `feature-tui-monolith` в `dev`~~ — смёржено (`b4c664f`), запись закрыта датой ниже.
|
||||
- Отслеживать тег `bootloader-v*` — после первого релиза закрыть запись «bootloader: полная реализация» датой и финальным SHA (сейчас часть диапазона — незакоммиченные изменения рабочего дерева).
|
||||
|
||||
## [Не выпущено] — bootloader: полная реализация, Фазы 0–6 (MCUboot Direct-XIP, HAB, service-tui интеграция)
|
||||
|
||||
Диапазон: `4644f21507d3..6c564f16388a` + незакоммиченные изменения рабочего дерева (HAB-подпись
|
||||
тестовым ключом, Тир-0/Тир-1 верификация в service-tui, CI `bootloader-v*`, документация)
|
||||
Сравнение: <https://github.com/OSabuser/tft_manufacture_test/compare/4644f21...6c564f1>
|
||||
|
||||
> `firmware/bootloader/` была пустой директорией на момент базового среза (см. "Что отслеживать"
|
||||
> выше — теперь снята с наблюдения, кроме тега релиза). Диапазон охватывает всю реализацию с нуля до
|
||||
> готовности к первому релизу (`VERSION 1.0.0`), шесть фаз согласно (уже удалённому после завершения,
|
||||
> см. "Удалено" ниже) `firmware/bootloader/PLAN.md`.
|
||||
|
||||
### Кратко
|
||||
|
||||
- Загрузчик MIMXRT1052 реализован целиком: XIP из Flash, выбор и запуск `tft_app` из одного из двух
|
||||
слотов (MCUboot Direct-XIP), обновление с microSD, устойчивость к зависшим образам (watchdog +
|
||||
recovery), HAB-подпись Release-сборки, интеграция с `service-tui` для контроля производственной
|
||||
прошивки.
|
||||
- По пути на реальном железе найдено и исправлено более десятка багов — от неверной трактовки
|
||||
регистров FlexSPI/SRC до архитектурных пробелов в чек-листах верификации; детали по фазам ниже.
|
||||
- `firmware/bootloader/CMakeLists.txt` → `VERSION 1.0.0`; все 6 фаз аппаратно верифицированы.
|
||||
|
||||
### Добавлено
|
||||
|
||||
- **Фаза 0 — карта Flash.** `docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md` — смещения
|
||||
`BOOTLOADER`/`SLOT_A`/`SLOT_B`, зафиксированы до написания кода.
|
||||
- **Фаза 1 — скелет.** `firmware/bootloader/{CMakeLists.txt,src/main.c,src/cli.c,src/protocol.{c,h}}` —
|
||||
bring-up (LED/tick/USB CDC), урезанный протокол (`ping`/`get_version`), HAB unsigned Debug-конфиг.
|
||||
- **Фаза 2 — bootutil (Direct-XIP).** `mcuboot_port/` — шим `flash_area_*` над `bsp_qspi_flash`,
|
||||
`sysflash.h`, `mcuboot_config.h` (TinyCrypt ECDSA-P256, `MCUBOOT_DIRECT_XIP_REVERT`),
|
||||
`src/boot_select.{c,h}`. `test_stub/` — заглушка `tft_app` (два слота, разная линковка) для
|
||||
аппаратной проверки выбора слота.
|
||||
- **Фаза 3 — SD-путь установки.** `src/{update_policy,slot_version,sd_update}.{c,h}` — сканирование
|
||||
microSD, установка в неактивный слот, top-level состояние «нет валидного образа»;
|
||||
`bootloader_fatfs` (read-only FatFS); аппаратный watchdog (`bsp/wdog`).
|
||||
- **Фаза 4 — SDRAM/QSPI smoke-test.** `bsp/sdram::bsp_sdram_configure()` — C-порт DCD (SEMC/CCM);
|
||||
`bsp_qspi_decode_chip()` — идентификация чипа по JEDEC; `src/led_status.{c,h}` — единый словарь
|
||||
LED-паттернов; `dev_sdram_test.c` (dev-only, `BOOTLOADER_DEV_DIAGNOSTICS`).
|
||||
- **Фаза 6 — recovery.** `bsp/boot_state` — счётчик попыток загрузки в `SRC_GPR3` (переживает
|
||||
watchdog-сброс, обнуляется на POR); `src/recovery.{c,h}` — чистая функция `recovery_decide()`
|
||||
(таксономия отказов A–D); recovery-режим по `BSP_BUTTON_2` с ослабленным version-gate.
|
||||
- **Фаза 5 — HAB Release + service-tui.** Тестовый HAB-ключ (`tools/host/hab/keys/`, схема NOCAK) —
|
||||
`hab_bootloader_release.yaml` реально подписывает Release-образ (`flags=0x08`);
|
||||
`tools/service_tui/app/bootloader_client.py` — CDC-клиент bootloader
|
||||
(`get_smoke_status`/`get_qspi_info`); `tools/service_tui/app/screens/verify.py` — экран живой
|
||||
проверки загрузчика после серийной прошивки (Тир-1); Тир-0 (readback-верификация записи,
|
||||
`flash_backend._verify_written()`) — включена по умолчанию для любой прошивки через `service-tui`;
|
||||
`firmware/bootloader/SIGNING_CEREMONY.md` — план настоящей production-подписи (HAB SRK + MCUboot
|
||||
production-ключ) на будущее.
|
||||
|
||||
### Изменено
|
||||
|
||||
- **Фаза 3**: детект SD консолидирован на единый `PRSSTAT`; ранний сэмпл кнопки даунгрейда.
|
||||
- **Фаза 4**: `board_mpu_init()` (общий для всех прошивок код) — добавлен Region 11 под NIC-301
|
||||
GPV-регистры (`0x41000000`, 8 МБ).
|
||||
- **Фаза 5**: `Flasher.PRODUCTION` (`tools/service_tui/app/flasher.py`) сужен до сценария A (только
|
||||
загрузчик) — бандл с `tft_app` (сценарий B) отложен до реализации `tft_app`; production жёстко
|
||||
резолвит Release, независимо от переменной `FIRMWARE_BUILD_TYPE`. `just host::package-tui` —
|
||||
новый явный гвард на `build/Release/bootloader_hab.bin`. `.github/workflows/release.yml` — тег
|
||||
`bootloader-v*` (симметрично `firmware-v*`), общий job `firmware` теперь собирает и подписывает
|
||||
Release-образ bootloader.
|
||||
- Корневой `README.md` — статус bootloader `запланирован` → `реализован, v1.0.0`.
|
||||
|
||||
### Исправлено
|
||||
|
||||
- **Фаза 2** (3 бага, аппаратная верификация): `jump_to_image()` маскировал IRQ перед прыжком (не по
|
||||
референсу NXP) — вешал `bsp_delay()` в любом целевом образе; `bsp_qspi_read()` не округлял
|
||||
`IDATSZ` до кратного 4 при IP-чтении — контроллер недодавал слово на хвостах не кратной длины
|
||||
(впервые проявилось на чтении хэша образа bootutil); `qspi_read_tail()` сравнивал
|
||||
`IPRXFSTS.FILL` (watermark-юниты по 8 байт) напрямую со счётчиком слов — зависал на хвостах ровно
|
||||
в 2 слова (чтение подписи ECDSA).
|
||||
- **Фаза 3**: форсированный даунгрейд физически записывался, но не загружался бы (`boot_go()` всегда
|
||||
выбирает более высокую версию) — добавлено поле `erase_previous_active`; `fih_panic_loop()`
|
||||
(вендоренный bootutil) ронял `test-host-release` в CI на x86_64-раннере (`invalid instruction
|
||||
mnemonic 'b'` — ARM/Thumb-only мнемоника, на arm64 devcontainer случайно ассемблировалась,
|
||||
маскируя проблему); стабы `test_stub` не позиционно-независимы — линковка под конкретный слот
|
||||
обязательна и для реального `tft_app`, не только для заглушки.
|
||||
- **Фаза 6** (4 бага): счётчик попыток загрузки рос и на пустой плате без SD (без реального
|
||||
зависания) — ошибочно уводил бы в recovery через ~4.5 с в штатном ожидании; в стенде `test_stub`
|
||||
health-mark вызывался безусловно до проверки `HANG_MODE`, из-за чего счётчик никогда не
|
||||
накапливался выше 1 (фолбэк не срабатывал); в самом чек-листе Фазы 6 предписывался файл для
|
||||
чужого слота при проверке recovery-установки; `attempt_boot()` не инкрементировал счётчик перед
|
||||
первым прыжком в свежеустановленный recovery-образ (симметрия с обычным путём).
|
||||
- **Фаза 4** (3 бага): AXI-QoS регистры (NIC-301 GPV) валили C-код фолтом — не покрыты
|
||||
`board_mpu_init()`, DCD успевал их записать до включения MPU, C-порт — нет; результаты
|
||||
smoke-теста терялись (шлются один раз сразу после `init()`, хост не успевает открыть порт) —
|
||||
кэширование + переспрос по команде; оценка длительности SDRAM-теста в комментарии оригинала
|
||||
завышена ~в 6 раз (реальный прогон ~4.2 с, не ~30 с).
|
||||
- **Фаза 5** (2 бага в `service-tui`, до публикации): production шил bootloader и (будущий) app по
|
||||
одному адресу `FLASH_BASE` — второй шаг затёр бы первый (наследие до-bootloader архитектуры, для
|
||||
Direct-XIP неверно); production мог тихо взять unsigned Debug-образ bootloader через
|
||||
`FIRMWARE_BUILD_TYPE` (переменная предназначена только для firmware_test, дефолт `Debug`) — теперь
|
||||
Release резолвится жёстко.
|
||||
|
||||
### Тесты
|
||||
|
||||
- Host-тесты выросли с 13 (Фаза 2) до 16 (Фазы 3/6: `update_policy`, `slot_version`, `recovery`) —
|
||||
зелёные, Debug и Release, обе платформы (macOS + devcontainer Linux).
|
||||
- `tools/service_tui`: 68 → 76 тестов (Фаза 5) — Тир-0 readback (`test_flash_backend.py`) + новый
|
||||
`test_bootloader_client.py`.
|
||||
- Полный аппаратный чек-лист пройден на каждой фазе (Фазы 2–6); детали были в удалённых
|
||||
`HARDWARE_VERIFICATION_*.md`/`DEBUG_LOG_*.md` (см. git-история, "Удалено" ниже).
|
||||
|
||||
### CI
|
||||
|
||||
- `.github/workflows/release.yml`: новый job `publish-bootloader` (тег `bootloader-v*`), общий job
|
||||
`firmware` расширен на сборку Release HAB bootloader; `service-tui-{macos,windows}` теперь
|
||||
докачивают `bootloader_hab.bin` в `build/Release/` перед упаковкой — без этого `just
|
||||
host::package-tui` падал бы с новым гвардом (см. "Изменено").
|
||||
- CI-баг `fih_panic_loop`/x86_64 (Фаза 3, см. "Исправлено") — точечный патч вендоренного
|
||||
`fault_injection_hardening.c` под `#if defined(__arm__)`.
|
||||
|
||||
### Документация
|
||||
|
||||
- `docs/bootloader/HAB_GUIDE.md` — новый §5.1 (разбор команд CSF-секции `nxpimage`, NOCAK vs полная
|
||||
SRK-иерархия).
|
||||
- `firmware/bootloader/SIGNING_CEREMONY.md` — новый план настоящей production-подписи (HAB
|
||||
SRK-церемония + MCUboot production-ключ), на будущее.
|
||||
- `docs/DEV_ARCH.md` (корневой) — исправлена фактическая ошибка (bootloader описывался как
|
||||
«копирование в ITCM», реально XIP без ITCM/DCD) и устаревший путь `tools/production/`.
|
||||
- `tools/service_tui/docs/DEV_ARCH.md` → переименован в `tools/service_tui/docs/ARCHITECTURE.md`
|
||||
(коллизия имени с корневым `docs/DEV_ARCH.md`); новый §16 (Тир-0/Тир-1); синхронизирован со всеми
|
||||
изменениями Фазы 5; починены 10 битых/несогласованных ссылок на файл в трёх других README.
|
||||
- `docs/CI_WORKFLOW.md` — синхронизирован с `bootloader-v*` (диаграмма, триггеры, таблица job'ов).
|
||||
|
||||
### Удалено
|
||||
|
||||
- `firmware/bootloader/PLAN.md`, `DEBUG_LOG_PHASE2.md`, `DEBUG_LOG_PHASE3_SD.md`,
|
||||
`test_stub/HARDWARE_VERIFICATION_{PHASE2,PHASE3,PHASE6,LED_PATTERNS}.md`, `docs/CI_PLAN.md` —
|
||||
планирующие/трекинговые документы и чек-листы, отработавшие своё после завершения всех фаз;
|
||||
фактическое содержание либо перенесено в постоянные документы (`README.md`,
|
||||
`docs/bootloader/HAB_GUIDE.md`, `docs/CI_WORKFLOW.md`), либо остаётся доступным в git-истории. Тот
|
||||
же паттерн, что уже применялся к `FIRST_RELEASE_PLAN.md`/`RELEASE_ROADMAP.md`/`just/ci_workflow.md`
|
||||
при предыдущем релизе (см. запись ниже).
|
||||
|
||||
## [2026-07-07 .. 2026-07-13] — Первый релиз: `tui-v0.2.0`/`firmware-v0.1.2`, точечный `tui-v0.2.1`
|
||||
|
||||
Диапазон: `8869b3c8..d9fb813b`
|
||||
Сравнение: <https://github.com/OSabuser/tft_manufacture_test/compare/8869b3c...d9fb813>
|
||||
|
||||
### Кратко
|
||||
|
||||
- Первый тег-релиз проекта: `firmware-v0.1.2` (firmware_test HAB Debug) и `tui-v0.2.0` (service-tui
|
||||
PyInstaller-бандл, macOS + Windows) — итог ветки `feature-tui-monolith` (см. запись ниже, закрыта
|
||||
этим же релизом).
|
||||
- Директория service-tui переименована `tools/production/` → `tools/service_tui/`.
|
||||
- Точечный релиз `tui-v0.2.1` (отдельная ветка `service-tui-fixes`) — найден и исправлен полевой баг
|
||||
записи во Flash, воспроизводившийся на случайном подмножестве плат.
|
||||
|
||||
### Изменено
|
||||
|
||||
- `tools/production/` → `tools/service_tui/` (директория и все внутренние пути/ссылки).
|
||||
- Из репозитория убран ранее случайно закоммиченный `dist/` (собранные PyInstaller-бандлы) —
|
||||
добавлен `.gitignore`.
|
||||
|
||||
### Исправлено
|
||||
|
||||
- **QE-бит (Winbond) не выставлялся при auto-config Flashloader — ~50/500 плат в поле падали на
|
||||
ЛЮБОЙ flash-операции.** Option word `0xC0000007` (со старта проекта, унаследован
|
||||
`flash_backend.py`/`flash_usb.py`) не включает Quad Enable; часть партий W25Q128 приходит с завода
|
||||
с QE=0, из-за чего чип остаётся в SPI-режиме при LUT, настроенных на quad-команды →
|
||||
`status 20106 FlexSPINOR: Command Failure` на любой команде. Две промежуточные гипотезы (порядок
|
||||
commit-FCB/erase; маргинальный электрический контакт) проверены на живом железе и опровергнуты.
|
||||
Причина найдена пересчётом (не «на глаз») десятичного option word из логов NXP MCUBootUtility:
|
||||
`0xC0000207`. QE энергонезависимый — после одной корректной установки (в т.ч. случайно, через
|
||||
сторонний инструмент) плата «чинится» навсегда, что и маскировало баг как нестабильный.
|
||||
- M5StampPLC: два раунда фиксов детекта порта и CAN-обмена в `service-tui`.
|
||||
|
||||
### Удалено
|
||||
|
||||
- Планирующие документы, отработавшие своё к моменту релиза — `FIRST_RELEASE_PLAN.md`,
|
||||
`RELEASE_ROADMAP.md`, `just/ci_workflow.md`, `tools/production/docs/MONOLITH_APP_PLAN.md`.
|
||||
Содержание перенесено в постоянные `README.md`/`docs/DEV_ARCH.md` (тогда ещё под именем
|
||||
`tools/production/`).
|
||||
|
||||
## [2026-07-07] — service-tui: монолитный spsdk-бэкенд, устойчивость к обрыву USB, упаковка PyInstaller
|
||||
|
||||
Диапазон: `1801f1beb959d610d31ee3dcd1f91046953117d4..22c40779ef0ec9911031d7a5272c4611b596d3e8` (мёрж в `dev` — `b4c664fe121226c4231675a150fba809e73b21d6`)
|
||||
Сравнение: <https://github.com/OSabuser/tft_manufacture_test/compare/1801f1beb959d610d31ee3dcd1f91046953117d4...22c40779ef0ec9911031d7a5272c4611b596d3e8>
|
||||
|
||||
> Ветка `feature-tui-monolith` (от `dev`, поверх мержа `feature-tui-python`). Смёржено в `dev` и
|
||||
> выпущено как часть первого релиза (`tui-v0.2.0`/`firmware-v0.1.2`) — см. запись выше.
|
||||
> **Полностью заменяет предыдущую версию этой записи**: прошивка сторонних
|
||||
> бинарников через subprocess (`tools/host/flash_usb.py --fcb-path` +
|
||||
> `nxpimage`) была реализацией на момент Фазы 0/раннего мержа и с тех пор
|
||||
> заменена прямыми вызовами `spsdk` Python API — ничего из старой записи
|
||||
> больше не описывает текущий код.
|
||||
|
||||
### Кратко
|
||||
|
||||
- Прошивка в `service-tui` переведена с subprocess-обёртки над
|
||||
`nxpimage`/`sdphost`/`blhost` на прямые вызовы `spsdk` Python API
|
||||
(`McuBoot`/`SDP`/`HabImage`) — `app/flash_backend.py`, провалидировано
|
||||
byte-exact на живом железе (macOS + Windows). `tools/host/flash_usb.py`
|
||||
остаётся отдельным dev-CLI для `just host::flash*`, TUI его больше не
|
||||
вызывает ни субпроцессом, ни как библиотеку.
|
||||
- Обрыв USB во время прошивки/chip erase теперь надёжно типизируется во
|
||||
всех трёх наблюдавшихся на железе сценариях (`SPSDKConnectionError`,
|
||||
`SPSDKTimeoutError`, `False`-по-таймауту без исключения) и даёт оператору
|
||||
единое понятное сообщение вместо «Непредвиденная ошибка».
|
||||
- Собран первый standalone-бандл (PyInstaller, onedir) — alpha, вручную
|
||||
протестирован на macOS и Windows.
|
||||
- Документация (`tools/production/README.md`+`docs/DEV_ARCH.md`, корневые
|
||||
`docs/*`, все `bsp/*/README.md`, корневой `README.md`) синхронизирована
|
||||
с фактическим состоянием кода после всех фаз миграции.
|
||||
|
||||
### Добавлено
|
||||
|
||||
- `tools/production/app/flash_backend.py` — синхронное ядро прошивки на
|
||||
spsdk: `detect_sdp`/`detect_cdc`, `load_flashloader`, `flash`,
|
||||
`erase_chip`, `build_custom_hab` (`HabImage` вместо `nxpimage` CLI),
|
||||
`write_fcb_explicit`/`write_fcb_auto`. Zero Textual/asyncio импортов,
|
||||
тестируется без event loop.
|
||||
- `tools/production/app/usb_ports.py` — `resolve_serial_port()` по VID:PID
|
||||
(имя порта не переносимо между перевтыкиваниями).
|
||||
- Иерархия `FlashBackendError`/`ConnectionLostError`/`DeviceNotFoundError`/
|
||||
`FlashLoaderTimeoutError`/`HabBuildError` с полем `connection_lost` —
|
||||
различает физический обрыв USB от логической ошибки прошивки без
|
||||
парсинга текста сообщения.
|
||||
- `tools/production/tests/test_flash_backend.py` — вырос до 45 unit-тестов
|
||||
backend'а, включая обе ветки обрыва USB (`SPSDKTimeoutError`,
|
||||
`False`-по-таймауту + вариант B через `detect_sdp()`) и golden-тест
|
||||
byte-exact сборки HAB.
|
||||
- Кнопка «✕ Выйти из приложения» на `WaitingScreen`.
|
||||
- `tools/production/service_tui.spec` — PyInstaller spec (onedir).
|
||||
- `tools/production/docs/RELEASE_ROADMAP.md` — дорожная карта Фаз
|
||||
4a→4b→5→6 с принятыми решениями (Р10–Р12) и статусом гейтов.
|
||||
|
||||
### Изменено
|
||||
|
||||
- `tools/production/app/flasher.py` — переведён с subprocess
|
||||
(`flash_usb.py` через `uv run`) на `asyncio.to_thread`-обёртку над
|
||||
`flash_backend.py`; сборка кастомного HAB — через `HabImage` в отдельном
|
||||
потоке, а не subprocess `nxpimage`.
|
||||
- `tools/production/app/main.py` — логирование: root по умолчанию `INFO`
|
||||
(было `DEBUG`), `spsdk`/`libusbsio` принудительно приглушены до
|
||||
`WARNING` независимо от root; полный DEBUG — через
|
||||
`SERVICE_LOG_LEVEL=DEBUG`.
|
||||
- `tools/production/app/screens/flash.py` — троттлинг записи в
|
||||
`#flash-log` для фазы `write` (раз на 10%, ~10 строк вместо ~135) без
|
||||
потери плавности прогресс-бара.
|
||||
- `bsp/sd/src/sd.c` — `bsp_sd_init()`/`bsp_sd_deinit()` теперь делают
|
||||
аппаратный `USDHC_Reset()` + полный `memset(&g_sd, ...)` перед
|
||||
повторной инициализацией: без этого non-blocking host driver SDK мог
|
||||
оставаться в состоянии ожидания транзакции от предыдущей
|
||||
diagnostic-сессии, и следующий `f_mount()` в тесте `usd` блокировался
|
||||
навсегда.
|
||||
|
||||
### Исправлено
|
||||
|
||||
- Обёртка обрыва USB расширена с `SPSDKConnectionError` на
|
||||
`(SPSDKConnectionError, SPSDKTimeoutError)` — второй тип не наследует
|
||||
первый, но реально прилетает на read-фазе после write.
|
||||
- Вариант B для команд, возвращающих `False` без исключения
|
||||
(`flash_erase_all`/`flash_erase_region`/`write_memory`): при `False`
|
||||
выполняется быстрый `detect_sdp()` — устройство пропало с шины →
|
||||
`ConnectionLostError`, устройство на месте → обычная `FlashBackendError`.
|
||||
- Баг «File not found» для bootloader/app/firmware_test при резолве путей
|
||||
прошивки (Фаза 4a).
|
||||
- Unit-тест моки (`test_cli.c`, `test_bsp_can.c`, `test_firmware_runner.c`,
|
||||
stub-хедеры `fsl_clock.h`/`version.h`) — фиксы после рефакторинга
|
||||
`cli.c`/`test_runner.c`.
|
||||
|
||||
### Тесты
|
||||
|
||||
- `test_flash_backend.py` — вырос до 45 тестов, включая гейт по
|
||||
`SPSDKTimeoutError` и переклассификации erase-таймаута (вариант B).
|
||||
|
||||
### Документация
|
||||
|
||||
- `tools/production/README.md`/`tools/production/docs/DEV_ARCH.md` —
|
||||
полностью пересмотрены под факт: убраны все следы subprocess/`nxpimage`/
|
||||
`flash_usb.py` из описания архитектуры прошивки; добавлены §6.2
|
||||
(обработка обрыва USB), §14 (PyInstaller/frozen-резолв путей), §15
|
||||
(логирование); зафиксирован разрыв между закоммиченным
|
||||
`service_tui.spec` (`datas` только `../shared`) и фактическим
|
||||
содержимым уже собранных релизных бандлов в `dist/`.
|
||||
- `docs/testing/PROTOCOL.md` — версия `0.1.0`→`0.1.2`, добавлена команда
|
||||
`get_version` и события `test_list`/`uid_response`/`version_response`,
|
||||
матрица тестов исправлена (убраны никогда не существовавшие `uart_ttl`/
|
||||
`uart_iso`, добавлен реальный `mqs`), поток Display дополнен шагами
|
||||
ротации (`display_rot0`/`display_rot_base`).
|
||||
- `docs/testing/host/HOST_CREATE_TEST.md` — был байт-в-байт дубликатом
|
||||
`docs/HOW_TO_DEBUG.md` (копипаст-баг, минимум с 2026-06-23); переписан
|
||||
как реальный гайд по добавлению host-теста.
|
||||
- `docs/HOW_TO_FLASH.md` (§1.5 под факт spsdk-конвейера), `docs/DEV_ARCH.md`
|
||||
(в дереве `tools/hil/` недоставало `04_test_button.py`),
|
||||
`docs/testing/hil/HIL_CREATE_TEST.md` (пример `loaded_<n>` без `m5`
|
||||
вводил в заблуждение — питание таргета всегда идёт через M5, не только
|
||||
сигнальные реле) — актуализированы.
|
||||
- `bsp/usb_cdc/README.md` (VID/PID был заявлен как заглушка `0x1234:0x0001`,
|
||||
реально прошит `0x1996:0x00AD`), `bsp/uart_host/README.md` (в списке API
|
||||
отсутствовали реальные `bsp_uart_host_deinit/rx_available/rx_flush`),
|
||||
`bsp/can/README.md` (несуществующие в коде `bsp_can.c`/`can_mock.h`/
|
||||
`bsp_can_rx_cb_t`) — исправлены по сверке с заголовками.
|
||||
- `bsp/mqs/{mqs.c,mqs.h,mqs_amp.c}` — докстринги приведены в соответствие
|
||||
с кодом (были «SAI1»/«16 кГц», реально SAI3/12 кГц — подтверждено
|
||||
сверкой с `bsp/generated/clock_config.c`); `bsp/provisioning/provisioning.h`
|
||||
— докстринг порядка байт UID исправлен на соответствующий реализации
|
||||
(`provisioning.c` пишет CFG0 первым, докстринг утверждал обратное).
|
||||
- Корневой `README.md` — `firmware/bootloader/`/`firmware/tft_app/`
|
||||
помечены как запланированные, а не готовые (директорий не существует,
|
||||
`add_subdirectory()` закомментирован в корневом `CMakeLists.txt`);
|
||||
добавлен ранее отсутствовавший раздел «Инструменты (`tools/`)» —
|
||||
`tools/production/` (service-tui) нигде не упоминался.
|
||||
|
||||
### Известные ограничения
|
||||
|
||||
- Auto-config Flashloader для W25Q256/512 не проверялся напрямую — решили
|
||||
не полагаться на него вообще, FCB для кастомных бинарей всегда пишется
|
||||
явно.
|
||||
- Массовое программирование (авто-прошивка по факту детекта SDP, без
|
||||
подтверждения оператора) рассмотрено и отклонено — в SDP/Flashloader-режиме
|
||||
нет способа прочитать UID платы для идентификации.
|
||||
|
||||
## [2026-06-29] — Этапы 6г–7: MQS, HIL pytest firmware_test, Provisioning
|
||||
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ if(NOT BUILD_TESTS_HOST)
|
|||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_C_EXTENSIONS OFF)
|
||||
set(BSP_SYSCALLS_FILE
|
||||
"${CMAKE_SOURCE_DIR}/bsp/generated/syscalls.c"
|
||||
CACHE FILEPATH "Заглушки системных вызовов newlib")
|
||||
"${CMAKE_SOURCE_DIR}/bsp/generated/syscalls.c"
|
||||
CACHE FILEPATH "Заглушки системных вызовов newlib")
|
||||
set(BSP_GENERATED
|
||||
"${CMAKE_SOURCE_DIR}/bsp/generated"
|
||||
CACHE PATH "Путь до сгенерированных ConfigTools файлов")
|
||||
"${CMAKE_SOURCE_DIR}/bsp/generated"
|
||||
CACHE PATH "Путь до сгенерированных ConfigTools файлов")
|
||||
set(BSP_STARTUP_FILE
|
||||
"${BSP_GENERATED}/startup/startup_MIMXRT1052.S"
|
||||
CACHE FILEPATH "Путь до стартап файла")
|
||||
"${BSP_GENERATED}/startup/startup_MIMXRT1052.S"
|
||||
CACHE FILEPATH "Путь до стартап файла")
|
||||
endif()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
|
@ -46,8 +46,10 @@ add_subdirectory(lib)
|
|||
# -----------------------------------------------------------------------------
|
||||
if(NOT BUILD_TESTS_HOST)
|
||||
add_subdirectory(firmware/test)
|
||||
# add_subdirectory(firmware/bootloader) - add_subdirectory(firmware/app) #
|
||||
# Загрузчик + Основное приложение
|
||||
add_subdirectory(firmware/bootloader)
|
||||
# Заглушка tft_app для аппаратной верификации bootutil (Фаза 2) — удалить,
|
||||
# когда появится реальный firmware/tft_app. См. firmware/bootloader/PLAN.md.
|
||||
add_subdirectory(firmware/bootloader/test_stub)
|
||||
endif()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -126,6 +126,19 @@
|
|||
"app"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mcuboot-stub-debug",
|
||||
"displayName": "mcuboot slot stub (Фаза 2/6 верификация) — Debug",
|
||||
"configurePreset": "Debug",
|
||||
"targets": [
|
||||
"test_slot_stub_a",
|
||||
"test_slot_stub_b",
|
||||
"test_slot_stub_a_hang",
|
||||
"test_slot_stub_b_hang",
|
||||
"test_slot_stub_a_confirm_hang",
|
||||
"test_slot_stub_b_confirm_hang"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "app-release",
|
||||
"displayName": "app — Release",
|
||||
|
|
@ -150,7 +163,11 @@
|
|||
"test_prio_queue",
|
||||
"uart_host_mock_example",
|
||||
"test_ring_buffer",
|
||||
"test_timeout_pattern"
|
||||
"test_timeout_pattern",
|
||||
"test_mcuboot_boot_select",
|
||||
"test_slot_version",
|
||||
"test_update_policy",
|
||||
"test_recovery"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -169,7 +186,11 @@
|
|||
"test_prio_queue",
|
||||
"uart_host_mock_example",
|
||||
"test_ring_buffer",
|
||||
"test_timeout_pattern"
|
||||
"test_timeout_pattern",
|
||||
"test_mcuboot_boot_select",
|
||||
"test_slot_version",
|
||||
"test_update_policy",
|
||||
"test_recovery"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
34
README.md
34
README.md
|
|
@ -9,11 +9,21 @@
|
|||
|
||||
## Firmware-проекты
|
||||
|
||||
| Проект | Путь | Описание |
|
||||
| ------------------- | ---------------------- | -------------------------------------------------------------------------------------- |
|
||||
| Тестовая прошивка | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS |
|
||||
| Загрузчик | `firmware/bootloader/` | A/B обновление через uSD. Обновляется только через USB ROM + blhost / SWD |
|
||||
| Production прошивка | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком |
|
||||
| Проект | Путь | Описание |
|
||||
| ----------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Тестовая прошивка | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS [README](firmware/test/README.md) |
|
||||
| Загрузчик | `firmware/bootloader/` | A/Б обновление через uSD (MCUboot, Direct-XIP), recovery при зависании образа. Обновляется только через USB ROM + blhost / SWD [README](firmware/bootloader/README.md) |
|
||||
| Production прошивка (запланирована) | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком |
|
||||
|
||||
---
|
||||
|
||||
## Инструменты (`tools/`)
|
||||
|
||||
| Инструмент | Путь | Назначение |
|
||||
| ------------------ | -------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| Сервисный TUI | `tools/service_tui/` | Диагностика и прошивка готовых плат сервисным инженером [README](tools/service_tui/README.md) |
|
||||
| Прошивка (dev-CLI) | `tools/host/` | USB SDP / SWD прошивка при разработке [README](tools/host/README.md) |
|
||||
| HIL-тесты | `tools/hil/` | pytest-окружение аппаратных тестов [README](tools/hil/README.md) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -58,20 +68,6 @@ 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) | `tools/host/uv.lock` |
|
||||
|
||||
Всё что не меняется — vendored. Сборка работает после `git clone` без интернета
|
||||
(кроме Python-зависимостей).
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@ if(BUILD_TESTS_HOST)
|
|||
return()
|
||||
endif()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# bsp_board — генерированные файлы Config Tools, инициализация платы
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# bsp_board — cгенерированные Config Tools файлы, инициализация платы
|
||||
add_library(bsp_board STATIC generated/board.c generated/pin_mux.c)
|
||||
|
||||
add_subdirectory(common)
|
||||
|
|
@ -15,6 +12,8 @@ add_subdirectory(uart_host)
|
|||
add_subdirectory(opto)
|
||||
add_subdirectory(can)
|
||||
add_subdirectory(button)
|
||||
add_subdirectory(wdog)
|
||||
add_subdirectory(boot_state)
|
||||
add_subdirectory(display)
|
||||
add_subdirectory(usb_cdc)
|
||||
add_subdirectory(sdram)
|
||||
|
|
@ -23,7 +22,6 @@ add_subdirectory(sd)
|
|||
add_subdirectory(mqs)
|
||||
add_subdirectory(provisioning)
|
||||
|
||||
# Подавляем предупреждения при компиляции собственных .c файлов библиотеки
|
||||
target_compile_options(bsp_board PRIVATE -w)
|
||||
|
||||
# SYSTEM подавляет предупреждения для всех внешних потребителей
|
||||
|
|
@ -66,3 +64,8 @@ target_compile_definitions(
|
|||
|
||||
add_library(bsp_boot_ram INTERFACE)
|
||||
target_compile_definitions(bsp_boot_ram INTERFACE SKIP_SYSCLK_INIT)
|
||||
|
||||
add_library(bsp_boot_xip_no_dcd INTERFACE)
|
||||
target_compile_definitions(
|
||||
bsp_boot_xip_no_dcd INTERFACE XIP_EXTERNAL_FLASH=1 XIP_BOOT_HEADER_ENABLE=1
|
||||
SKIP_SYSCLK_INIT)
|
||||
|
|
|
|||
14
bsp/boot_state/CMakeLists.txt
Normal file
14
bsp/boot_state/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
if(BUILD_TESTS_HOST)
|
||||
return()
|
||||
endif()
|
||||
|
||||
add_library(bsp_boot_state STATIC src/boot_state.c)
|
||||
|
||||
target_include_directories(
|
||||
bsp_boot_state
|
||||
PUBLIC include/
|
||||
PRIVATE src/)
|
||||
|
||||
target_link_libraries(
|
||||
bsp_boot_state
|
||||
PRIVATE sdk_src)
|
||||
79
bsp/boot_state/README.md
Normal file
79
bsp/boot_state/README.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# bsp_boot_state — счётчик попыток загрузки (SRC_GPR)
|
||||
|
||||
Счётчик попыток загрузки поверх `SRC` General Purpose Register — переживает тёплый/watchdog-сброс,
|
||||
обнуляется только на POR. Даёт коду восстановления прожить несколько сбросов подряд без
|
||||
персистентного хранилища во flash.
|
||||
|
||||
---
|
||||
|
||||
## Аппаратура
|
||||
|
||||
| Параметр | Значение |
|
||||
| ------------------------ | -------------------------------------------------------------- |
|
||||
| Периферия | SRC (System Reset Controller) |
|
||||
| Регистр счётчика | `SRC_GPR[2]` (GPR3, 0-based индекс `fsl_src` API) |
|
||||
| Переживает | тёплый сброс, watchdog-сброс |
|
||||
| Обнуляется | только POR (детект — `SRC->SRSR`, бит `IPP_RESET_B`) |
|
||||
| Занято ROM (не трогать) | GPR1/2 (warm-boot entry/arg), GPR6/7/8/9 (ROM, explicit note в RM), GPR10 (альт. SBMR1) |
|
||||
| Не занято ROM, но занято конвенцией | GPR5 — RM рекомендует под различение SYSRESETREQ/CPU lockup, не наша задача |
|
||||
|
||||
---
|
||||
|
||||
## Контракт: разделение с `bsp_wdog`
|
||||
|
||||
`SRC->SRSR` и `WDOG1->WRSR` — разные регистры с разной семантикой очистки. `WRSR` самоочищается на
|
||||
каждый сброс (не требует явной очистки — см. `bsp_wdog_caused_last_reset()`). `SRSR` —
|
||||
write-1-to-clear и **копит биты между тёплыми сбросами**, если их не чистить софтом:
|
||||
`bsp_boot_state_init()` чистит `SRSR` при каждом вызове, поэтому вопрос «был ли сброс по watchdog»
|
||||
остаётся за `bsp_wdog`, а не за этим модулем — этот модуль отвечает только за «был ли сброс POR».
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```c
|
||||
void bsp_boot_state_init(void); /* взвести — читает/чистит SRC->SRSR, детектит POR */
|
||||
bool bsp_boot_state_was_por(void); /* true, если последний сброс — POR */
|
||||
uint32_t bsp_boot_attempt_count(void); /* текущее значение счётчика, 0 сразу после POR */
|
||||
void bsp_boot_attempt_inc(void); /* +1 — звать перед попыткой прыжка в образ */
|
||||
void bsp_boot_attempt_reset(void); /* обнулить — новый образ/фолбэк-стирание */
|
||||
void bsp_boot_health_mark(void); /* = bsp_boot_attempt_reset(), для вызова из приложения */
|
||||
```
|
||||
|
||||
`bsp_boot_state_init()` **не идемпотентна** — повторный вызов в той же сессии увидит уже очищенный
|
||||
`SRSR` как «не POR». Звать ровно один раз, как можно раньше в `main()`.
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```c
|
||||
#include "bsp/boot_state.h"
|
||||
|
||||
/* main.c — после board_hw_init()/bsp_wdog_init(): */
|
||||
bsp_boot_state_init();
|
||||
|
||||
if (bsp_boot_attempt_count() >= THRESHOLD)
|
||||
{
|
||||
/* серия сбросов подряд без здорового образа — решение о фолбэке/recovery
|
||||
* принимает вызывающий код, не этот модуль */
|
||||
}
|
||||
|
||||
bsp_boot_attempt_inc(); /* перед каждой попыткой прыжка */
|
||||
/* ... */
|
||||
bsp_boot_health_mark(); /* приложение подтвердило собственное здоровье */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CMake
|
||||
|
||||
```cmake
|
||||
target_link_libraries(firmware_bootloader PRIVATE bsp_boot_state)
|
||||
```
|
||||
|
||||
**Зависимости модуля:**
|
||||
|
||||
| Зависимость | Тип | Описание |
|
||||
| ----------- | ------- | -------------------------------------------------------------- |
|
||||
| `sdk_src` | PRIVATE | `fsl_src.h` — `SRC_Get/SetGeneralPurposeRegister`, `SRC_Get/ClearResetStatusFlags` |
|
||||
59
bsp/boot_state/include/bsp/boot_state.h
Normal file
59
bsp/boot_state/include/bsp/boot_state.h
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* bsp_boot_state — счётчик попыток загрузки поверх SRC General Purpose
|
||||
* Register (MIMXRT1052).
|
||||
*
|
||||
* Назначение: пережить несколько сбросов подряд без реального
|
||||
* персистентного хранилища во flash. SRC_GPR — retained-регистр: сохраняет
|
||||
* значение через тёплый/watchdog-сброс, теряет его только на POR.
|
||||
*
|
||||
* Контракт: bsp_boot_state_init() сама детектирует POR по SRC->SRSR и, если
|
||||
* это POR, обнуляет счётчик — вызывающему коду не нужно проверять причину
|
||||
* сброса самостоятельно для этой цели. Причина «сброс по watchdog?» — не
|
||||
* этот модуль, см. bsp_wdog_caused_last_reset() (bsp/wdog.h): SRC->SRSR и
|
||||
* WDOG1->WRSR — разные регистры с разной семантикой очистки (WRSR
|
||||
* самоочищается на каждый сброс, SRSR — write-1-to-clear, накапливает биты
|
||||
* между сбросами без явной очистки).
|
||||
*/
|
||||
|
||||
#ifndef BSP_BOOT_STATE_H
|
||||
#define BSP_BOOT_STATE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* Инициализация. Вызывать один раз, как можно раньше в main() (после
|
||||
* board_hw_init()/bsp_wdog_init()). Читает SRC->SRSR, определяет POR (бит
|
||||
* IPP_RESET_B — на MIMXRT1052 отдельного бита POR нет, эту роль играет он),
|
||||
* чистит SRSR (write-1-to-clear, иначе биты копятся между тёплыми сбросами)
|
||||
* и, если это POR, обнуляет счётчик попыток.
|
||||
*
|
||||
* Повторный вызов в той же сессии — не идемпотентен (снова прочитает и
|
||||
* очистит уже очищенный SRSR, увидит "не POR"): звать ровно один раз.
|
||||
*/
|
||||
void bsp_boot_state_init(void);
|
||||
|
||||
/* true, если последний сброс МК (на момент bsp_boot_state_init()) был POR. */
|
||||
bool bsp_boot_state_was_por(void);
|
||||
|
||||
/* Текущее значение счётчика попыток. 0 сразу после POR. */
|
||||
uint32_t bsp_boot_attempt_count(void);
|
||||
|
||||
/* Инкремент счётчика попыток. Звать перед каждой попыткой прыжка в образ. */
|
||||
void bsp_boot_attempt_inc(void);
|
||||
|
||||
/*
|
||||
* Обнулить счётчик попыток. Звать при: успешной установке нового образа
|
||||
* (свежему образу — полный бюджет попыток), фолбэк-стирании зависшего слота
|
||||
* (ситуация изменилась).
|
||||
*/
|
||||
void bsp_boot_attempt_reset(void);
|
||||
|
||||
/*
|
||||
* Отметить образ здоровым — семантический алиас bsp_boot_attempt_reset() для
|
||||
* вызова из приложения (tft_app), а не из загрузчика: тот же эффект (счётчик
|
||||
* обнуляется), но имя называет намерение вызывающей стороны.
|
||||
*/
|
||||
void bsp_boot_health_mark(void);
|
||||
|
||||
#endif /* BSP_BOOT_STATE_H */
|
||||
67
bsp/boot_state/src/boot_state.c
Normal file
67
bsp/boot_state/src/boot_state.c
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* bsp_boot_state — реализация поверх fsl_src (SRC_GPR5 + SRC->SRSR).
|
||||
*/
|
||||
|
||||
#include "bsp/boot_state.h"
|
||||
|
||||
#include "fsl_src.h"
|
||||
|
||||
#define BSP_BOOT_STATE_SRC_BASE SRC
|
||||
|
||||
/*
|
||||
* GPR-индекс счётчика попыток (0-based, index=0 -> GPR1). Сверено с i.MX RT1050
|
||||
* RM (SRC, гл. 21.8.5-21.8.13): GPR1/2 — ROM (entry/arg пробуждения из
|
||||
* low-power), GPR6/7/8/9 — ТОЖЕ explicit "used by the ROM code, should not be
|
||||
* used by application software" (несмотря на первоначальное предположение,
|
||||
* что свободны только GPR1/2/10), GPR10 — ROM (альтернативный SBMR1 через бит
|
||||
* [28]). GPR5 формально не ROM, но RM рекомендует именно его под отдельную
|
||||
* задачу (различение SYSRESETREQ/CPU lockup) — не занимаем во избежание
|
||||
* конфликта с этой конвенцией. GPR3 (index 2) — единственный не подписан
|
||||
* НИКАКИМ примечанием в RM, описан просто как "arbitrary value".
|
||||
*/
|
||||
#define BSP_BOOT_STATE_GPR_INDEX 2U
|
||||
|
||||
static bool g_s_was_por = false;
|
||||
|
||||
void bsp_boot_state_init(void)
|
||||
{
|
||||
uint32_t flags = SRC_GetResetStatusFlags(BSP_BOOT_STATE_SRC_BASE);
|
||||
|
||||
g_s_was_por = (flags & (uint32_t) kSRC_IppResetPinFlag) != 0U;
|
||||
|
||||
/* SRSR — write-1-to-clear, копит биты между тёплыми сбросами без явной
|
||||
* очистки (в отличие от WDOG1->WRSR, который самоочищается). Чистим всё,
|
||||
* что доступно, чтобы следующая загрузка увидела только свою причину. */
|
||||
SRC_ClearResetStatusFlags(BSP_BOOT_STATE_SRC_BASE, ~0U);
|
||||
|
||||
if (g_s_was_por)
|
||||
{
|
||||
bsp_boot_attempt_reset();
|
||||
}
|
||||
}
|
||||
|
||||
bool bsp_boot_state_was_por(void)
|
||||
{
|
||||
return g_s_was_por;
|
||||
}
|
||||
|
||||
uint32_t bsp_boot_attempt_count(void)
|
||||
{
|
||||
return SRC_GetGeneralPurposeRegister(BSP_BOOT_STATE_SRC_BASE, BSP_BOOT_STATE_GPR_INDEX);
|
||||
}
|
||||
|
||||
void bsp_boot_attempt_inc(void)
|
||||
{
|
||||
uint32_t count = bsp_boot_attempt_count();
|
||||
SRC_SetGeneralPurposeRegister(BSP_BOOT_STATE_SRC_BASE, BSP_BOOT_STATE_GPR_INDEX, count + 1U);
|
||||
}
|
||||
|
||||
void bsp_boot_attempt_reset(void)
|
||||
{
|
||||
SRC_SetGeneralPurposeRegister(BSP_BOOT_STATE_SRC_BASE, BSP_BOOT_STATE_GPR_INDEX, 0U);
|
||||
}
|
||||
|
||||
void bsp_boot_health_mark(void)
|
||||
{
|
||||
bsp_boot_attempt_reset();
|
||||
}
|
||||
|
|
@ -1,24 +1,6 @@
|
|||
/*
|
||||
* bsp_button — тактовые кнопки TactBut1 / TactBut2
|
||||
*
|
||||
* Аппаратура:
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ bsp_status_t bsp_can_set_filter(uint8_t idx, uint32_t id,
|
|||
uint32_t mask, bool is_extended);
|
||||
bsp_status_t bsp_can_accept_all(void);
|
||||
|
||||
bsp_status_t bsp_can_register_rx_callback(bsp_can_rx_cb_t cb, void *p_ctx);
|
||||
bsp_status_t bsp_can_register_rx_callback(bsp_can_rx_callback_t cb, void *p_ctx);
|
||||
```
|
||||
|
||||
**Коды возврата `bsp_can_send()`:**
|
||||
|
|
@ -147,7 +147,7 @@ bsp_can_set_filter(2, 0x1ABCDEF0, 0x1FFFFFFF, true);
|
|||
add_host_test(
|
||||
NAME test_bsp_can
|
||||
SOURCES can/test_bsp_can.c
|
||||
${PROJECT_SOURCE_DIR}/bsp/can/src/bsp_can.c
|
||||
${PROJECT_SOURCE_DIR}/bsp/can/src/can.c
|
||||
${PROJECT_SOURCE_DIR}/utils/ring_buffer/ring_buffer.c
|
||||
INCLUDES
|
||||
${PROJECT_SOURCE_DIR}/bsp/can/include
|
||||
|
|
@ -160,7 +160,7 @@ add_host_test(
|
|||
**Humble Object** — fff-заглушки для потребителей в `bsp/can/mocks/`:
|
||||
|
||||
```c
|
||||
#include "can_mock.h"
|
||||
#include "can_mocks.h"
|
||||
|
||||
void setUp(void) { CAN_MOCK_RESET_ALL(); }
|
||||
|
||||
|
|
|
|||
|
|
@ -161,18 +161,15 @@ static uint32_t poll_rx_mailboxes(void)
|
|||
|
||||
uint8_t mb_idx = RX_MB_FIRST + i;
|
||||
|
||||
/* Проверяем флаг готовности MB. */
|
||||
uint64_t mb_flag = (uint64_t) 1U << mb_idx;
|
||||
if (FLEXCAN_GetMbStatusFlags(BSP_CAN_BASE, mb_flag) == 0U)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Читаем фрейм из MB. */
|
||||
flexcan_frame_t sdk_frame;
|
||||
status_t sdk_status = FLEXCAN_ReadRxMb(BSP_CAN_BASE, mb_idx, &sdk_frame);
|
||||
|
||||
/* Очищаем флаг. */
|
||||
FLEXCAN_ClearMbStatusFlags(BSP_CAN_BASE, mb_flag);
|
||||
|
||||
if ((sdk_status == kStatus_Success) || (sdk_status == kStatus_FLEXCAN_RxOverflow))
|
||||
|
|
@ -180,7 +177,6 @@ static uint32_t poll_rx_mailboxes(void)
|
|||
bsp_can_frame_t bsp_frame;
|
||||
frame_from_sdk(&sdk_frame, &bsp_frame);
|
||||
|
||||
/* Сериализуем фрейм побайтово в ring buffer. */
|
||||
ring_buffer_write(&g_s_rx_ring, (const uint8_t *) &bsp_frame, sizeof(bsp_frame));
|
||||
received++;
|
||||
}
|
||||
|
|
@ -222,16 +218,13 @@ bsp_status_t bsp_can_init(const bsp_can_config_t *p_config)
|
|||
return BSP_ERR_PARAM;
|
||||
}
|
||||
|
||||
/* Если уже инициализирован — сначала деинициализируем. */
|
||||
if (g_s_initialized)
|
||||
{
|
||||
bsp_can_deinit();
|
||||
}
|
||||
|
||||
/* Инициализация ring buffer. */
|
||||
ring_buffer_init(&g_s_rx_ring, g_s_rx_ring_storage, RX_RING_SIZE);
|
||||
|
||||
/* Конфигурация FlexCAN. */
|
||||
flexcan_config_t flexcan_cfg;
|
||||
FLEXCAN_GetDefaultConfig(&flexcan_cfg);
|
||||
|
||||
|
|
@ -306,7 +299,6 @@ bsp_status_t bsp_can_set_filter(uint8_t index, uint32_t can_id, uint32_t mask, b
|
|||
|
||||
uint8_t mb_idx = RX_MB_FIRST + index;
|
||||
|
||||
/* Конфигурация RX MB. */
|
||||
flexcan_rx_mb_config_t rx_mb_cfg;
|
||||
rx_mb_cfg.type = kFLEXCAN_FrameTypeData;
|
||||
|
||||
|
|
@ -422,14 +414,12 @@ bsp_status_t bsp_can_send(const bsp_can_frame_t *p_frame, uint32_t timeout_ms)
|
|||
flexcan_frame_t sdk_frame;
|
||||
frame_to_sdk(p_frame, &sdk_frame);
|
||||
|
||||
/* Записать фрейм в TX MB. */
|
||||
status_t wr_status = FLEXCAN_WriteTxMb(BSP_CAN_BASE, TX_MB_IDX, &sdk_frame);
|
||||
if (wr_status != kStatus_Success)
|
||||
{
|
||||
return BSP_ERR_BUSY;
|
||||
}
|
||||
|
||||
/* Ждать завершения передачи с таймаутом. */
|
||||
uint64_t tx_flag = (uint64_t) 1U << TX_MB_IDX;
|
||||
uint32_t start_ms = bsp_tick_get_ms();
|
||||
|
||||
|
|
@ -442,7 +432,6 @@ bsp_status_t bsp_can_send(const bsp_can_frame_t *p_frame, uint32_t timeout_ms)
|
|||
}
|
||||
}
|
||||
|
||||
/* Очистить флаг завершения. */
|
||||
FLEXCAN_ClearMbStatusFlags(BSP_CAN_BASE, tx_flag);
|
||||
|
||||
return BSP_OK;
|
||||
|
|
@ -471,16 +460,13 @@ bsp_status_t bsp_can_receive(bsp_can_frame_t *p_frame, uint32_t timeout_ms)
|
|||
|
||||
for (;;)
|
||||
{
|
||||
/* Опросить все активные MB, сложить в ring buffer. */
|
||||
poll_rx_mailboxes();
|
||||
|
||||
/* Попробовать извлечь фрейм. */
|
||||
if (try_dequeue_frame(p_frame))
|
||||
{
|
||||
return BSP_OK;
|
||||
}
|
||||
|
||||
/* Проверить таймаут. */
|
||||
uint32_t elapsed = bsp_tick_get_ms() - start_ms;
|
||||
if (elapsed >= timeout_ms)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
* @file display.h
|
||||
* @brief BSP: ELCDIF display driver — TFT4 / TFT7 / TFT8 / TFT10.
|
||||
*
|
||||
* Bare-metal compatible: callback-based FRAME_DONE notification.
|
||||
* No FreeRTOS dependency in this layer.
|
||||
*/
|
||||
|
||||
#ifndef BSP_DISPLAY_DISPLAY_H_
|
||||
|
|
|
|||
|
|
@ -91,6 +91,16 @@ static void board_mpu_init(void)
|
|||
MPU->RBAR = ARM_MPU_RBAR(10U, 0x40000000U);
|
||||
MPU->RASR = ARM_MPU_RASR(0U, ARM_MPU_AP_FULL, 2U, 0U, 0U, 0U, 0U, ARM_MPU_REGION_SIZE_4MB);
|
||||
|
||||
/* Region 11: Device, NIC-301 GPV (bus-arbitration QoS) 0x41000000, 8 MB.
|
||||
* Region 10 не покрывает — GPV0/SIM_MAIN (0x41000000) и GPV4/SIM_M7
|
||||
* (0x41400000, "Cortex-M7 read/write_qos") лежат за пределами его 4 MB
|
||||
* от 0x40000000, попадают только под Region 0 (deny-all, errata-воркэраунд
|
||||
* выше) → запись фолтит, хотя регистры реальные и документированы (i.MX
|
||||
* RT1050 RM, гл. 29 "Network Interconnect Bus System (NIC-301)"). Нужен
|
||||
* bsp_sdram_configure() для read_qos/write_qos регионов SDRAM/LCD/M7. */
|
||||
MPU->RBAR = ARM_MPU_RBAR(11U, 0x41000000U);
|
||||
MPU->RASR = ARM_MPU_RASR(0U, ARM_MPU_AP_FULL, 2U, 0U, 0U, 0U, 0U, ARM_MPU_REGION_SIZE_8MB);
|
||||
|
||||
ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
|
||||
|
||||
SCB_EnableDCache();
|
||||
|
|
|
|||
|
|
@ -28,9 +28,27 @@ static sd_io_voltage_t s_io_voltage = {
|
|||
volatile uint32_t g_sdmmc_dbg_dma_buf_addr = 0U;
|
||||
volatile uint32_t g_sdmmc_dbg_usdhc1_src_clock_hz = 0U;
|
||||
|
||||
static bool sd_card_detect_gpio(void)
|
||||
/*
|
||||
* Детект карты через USDHC PRES_STATE.CINST — ЕДИНЫЙ механизм и для нашего
|
||||
* гейта bsp_sd_is_inserted() (bsp/sd/src/sd.c), и для внутреннего
|
||||
* SD_PollingCardInsert() SDK (тот зовёт этот callback при kSD_DetectCardByGpioCD).
|
||||
* Не GPIO_PinRead — исторически детект через GPIO2/28 давал ложный "card
|
||||
* present" на пустом слоте (Фаза 3, симптом 1; DEBUG_LOG_PHASE3_SD.md, раунд 3).
|
||||
* Работает, пока пин D13 замаплен на USDHC1_CD_B (см. BOARD_SD_Config ниже:
|
||||
* прежний remux на GPIO2_IO28 убран, пин остаётся на USDHC1_CD_B постоянно —
|
||||
* консолидация, item 1). Тактирование USDHC1 включается идемпотентно на случай
|
||||
* вызова до полного SD_HostInit().
|
||||
*
|
||||
* Тот же PRSSTAT-бит читает и штатный host-CD путь SDK
|
||||
* (SDMMCHOST_CardDetectStatus), но kSD_DetectCardByHostCD дополнительно взводит
|
||||
* USDHC card-detect ПРЕРЫВАНИЯ — не нужны загрузчику (лишний источник IRQ перед
|
||||
* прыжком), поэтому оставляем polling через callback (kSD_DetectCardByGpioCD).
|
||||
*/
|
||||
static bool sd_card_detect_prsstat(void)
|
||||
{
|
||||
return GPIO_PinRead(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN) == BOARD_SDMMC_SD_CD_INSERT_LEVEL;
|
||||
CLOCK_EnableClock(kCLOCK_Usdhc1);
|
||||
return (USDHC_GetPresentStatusFlags(BOARD_SDMMC_SD_HOST_BASEADDR) &
|
||||
(uint32_t) kUSDHC_CardInsertedFlag) != 0U;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
|
|
@ -49,7 +67,8 @@ static uint32_t get_usdhc1_src_clock_hz(void)
|
|||
}
|
||||
|
||||
/*
|
||||
* Управление питанием карты: GPIO1[19] (SdPwr), active-high.
|
||||
* Управление питанием карты: GPIO1[19] (SdPwr). Регистрируется как
|
||||
* usrParam.pwr — SDK дёргает её из SD_SetCardPower().
|
||||
*/
|
||||
static void sd_power_control(bool enable)
|
||||
{
|
||||
|
|
@ -123,10 +142,12 @@ static void sd_pin_config(uint32_t freq)
|
|||
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_03_USDHC1_DATA1, pad);
|
||||
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_04_USDHC1_DATA2, pad);
|
||||
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_05_USDHC1_DATA3, pad);
|
||||
/* CD_B в GPIO-режиме: подтяжка вверх + hysteresis для стабильного уровня. */
|
||||
IOMUXC_SetPinConfig(IOMUXC_GPIO_B1_12_GPIO2_IO28,
|
||||
IOMUXC_SW_PAD_CTL_PAD_PKE_MASK | IOMUXC_SW_PAD_CTL_PAD_PUE_MASK |
|
||||
IOMUXC_SW_PAD_CTL_PAD_HYS_MASK | IOMUXC_SW_PAD_CTL_PAD_PUS(1));
|
||||
/*
|
||||
* CD (D13) здесь НЕ конфигурируем: пин на USDHC1_CD_B (см. BOARD_SD_Config),
|
||||
* pad задан в BOARD_InitPins() и на железе даёт корректный CINST. Прежняя
|
||||
* настройка pad'а GPIO2_IO28 убрана вместе с GPIO-детектом (item 1,
|
||||
* DEBUG_LOG_PHASE3_SD.md).
|
||||
*/
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
|
|
@ -151,10 +172,10 @@ void BOARD_SD_Config(void *card, sd_cd_t cd, uint32_t host_irq_priority, void *u
|
|||
g_sdmmc_dbg_dma_buf_addr = (uint32_t)(uintptr_t)s_dma_buf;
|
||||
g_sdmmc_dbg_usdhc1_src_clock_hz = sd->host->hostController.sourceClock_Hz;
|
||||
|
||||
/* --- card detect: GPIO CD (active-low) --- */
|
||||
/* --- card detect: USDHC PRES_STATE.CINST через callback (polling, без IRQ) --- */
|
||||
s_cd.cdDebounce_ms = BOARD_SDMMC_SD_CD_DEBOUNCE_MS;
|
||||
s_cd.type = BOARD_SDMMC_SD_CD_TYPE;
|
||||
s_cd.cardDetected = sd_card_detect_gpio;
|
||||
s_cd.type = BOARD_SDMMC_SD_CD_TYPE; /* kSD_DetectCardByGpioCD → callback ниже */
|
||||
s_cd.cardDetected = sd_card_detect_prsstat;
|
||||
s_cd.callback = cd; /* обычно NULL из bsp_sd */
|
||||
s_cd.userData = user_data;
|
||||
|
||||
|
|
@ -166,15 +187,20 @@ void BOARD_SD_Config(void *card, sd_cd_t cd, uint32_t host_irq_priority, void *u
|
|||
|
||||
/* --- GPIO питания --- */
|
||||
sd_power_init();
|
||||
/* CD_B: переводим в GPIO2_IO28 и настраиваем вход */
|
||||
IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_GPIO2_IO28, 0U);
|
||||
const gpio_pin_config_t cd_cfg = {
|
||||
.direction = kGPIO_DigitalInput,
|
||||
.outputLogic = 0U,
|
||||
.interruptMode = kGPIO_NoIntmode,
|
||||
};
|
||||
GPIO_PinInit(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN, &cd_cfg);
|
||||
/* Важно: применяем pad-конфиг сразу для ранних CMD (CMD0/CMD8/CMD55/ACMD41). */
|
||||
/*
|
||||
* CD_B (GPIO_B1_12 / physical D13) остаётся на USDHC1_CD_B постоянно —
|
||||
* единый механизм детекта через PRES_STATE.CINST (sd_card_detect_prsstat
|
||||
* выше + гейт bsp_sd_is_inserted). Прежней двойной маршрутизации
|
||||
* (remux на GPIO2_IO28 для GPIO-чтения внутри f_mount) больше нет —
|
||||
* см. DEBUG_LOG_PHASE3_SD.md, раунд 3 «консолидация детекта» (item 1);
|
||||
* она убирала латентную хрупкость: после первого bsp_sd_init() пин уходил
|
||||
* на GPIO2_IO28 и повторный PRSSTAT-скан ослеп бы. Явно переустанавливаем
|
||||
* альт-функцию (BOARD_InitPins() её тоже ставит — так модуль не зависит от
|
||||
* порядка инициализации). Pad этого пина оставляем как задал BOARD_InitPins:
|
||||
* на железе CINST на нём читается корректно (Фаза 3, все сценарии).
|
||||
*/
|
||||
IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_USDHC1_CD_B, 0U);
|
||||
/* Pad-конфиг линий SD (CMD/CLK/DATA) сразу для ранних CMD (CMD0/CMD8/CMD55/ACMD41). */
|
||||
sd_pin_config(400000U);
|
||||
|
||||
/* --- приоритет прерывания хоста --- */
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@
|
|||
* LED_HEARTBEAT — системный, мигает как признак жизни прошивки
|
||||
* LED_APP — прикладной, управляется из firmware по ситуации
|
||||
*
|
||||
* Пины сконфигурированы в generated/pin_mux.h. Этот хедер не знает
|
||||
* ни про GPIO-порты, ни про NXP SDK.
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
/**
|
||||
* @file bsp/mqs.h
|
||||
* @brief BSP: Medium Quality Sound (MQS) — SAI1 + eDMA + MQS.
|
||||
* @brief BSP: Medium Quality Sound (MQS) — SAI3 + eDMA + MQS.
|
||||
*
|
||||
* Слой абстракции над SAI1/eDMA/MQS для монофонического аудио-выхода.
|
||||
* Слой абстракции над SAI3/eDMA/MQS для монофонического аудио-выхода.
|
||||
* Физически на плате выведен один канал (MQS_RIGHT, GPIO_AD_B0_04);
|
||||
* SAI1 требует стерео-буфер — оба канала всегда идентичны.
|
||||
* SAI3 требует стерео-буфер — оба канала всегда идентичны.
|
||||
*
|
||||
* Режимы использования:
|
||||
* - firmware_test: bsp_mqs_play_blocking() — синхронная подача
|
||||
|
|
@ -38,14 +38,14 @@ extern "C"
|
|||
* Параметры аудио-потока
|
||||
* ----------------------------------------------------------------------- */
|
||||
|
||||
/** Частота дискретизации, Гц. Небольшое отклонение (~0.5%) из-за
|
||||
* источника SAI1_CLK_ROOT (System PLL PFD2, не Audio PLL). */
|
||||
/** Частота дискретизации, Гц. Источник — SAI3_CLK_ROOT (Audio PLL / 8 / 8),
|
||||
* делитель MCLK подобран точно (8), отклонения нет. */
|
||||
#define BSP_MQS_SAMPLE_RATE_HZ (44100U)
|
||||
|
||||
/** Разрядность PCM. MQS поддерживает только 16 бит. */
|
||||
#define BSP_MQS_BIT_WIDTH (16U)
|
||||
|
||||
/** Количество каналов в буфере. SAI1+MQS требует стерео; правый == левый. */
|
||||
/** Количество каналов в буфере. SAI3+MQS требует стерео; правый == левый. */
|
||||
#define BSP_MQS_CHANNELS (2U)
|
||||
|
||||
/** Байт на один моно-сэмпл (16 бит → 2 байта). */
|
||||
|
|
@ -73,11 +73,11 @@ extern "C"
|
|||
* ----------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Инициализация MQS-подсистемы: SAI1, eDMA, DMAMUX, MQS.
|
||||
* @brief Инициализация MQS-подсистемы: SAI3, eDMA, DMAMUX, MQS.
|
||||
*
|
||||
* Включает тактирование SAI1 (kCLOCK_Sai1), настраивает SAI1 в режиме
|
||||
* Включает тактирование SAI3 (kCLOCK_Sai3), настраивает SAI3 в режиме
|
||||
* TX Master, 16 бит, стерео, 44100 Гц, инициализирует eDMA канал 0
|
||||
* (DMAMUX source kDmaRequestMuxSai1Tx) и MQS-модуль.
|
||||
* (DMAMUX source kDmaRequestMuxSai3Tx) и MQS-модуль.
|
||||
*
|
||||
* Пин GPIO_AD_B0_04 (MQS_RIGHT) уже замультиплексирован в BOARD_InitPins().
|
||||
* MQS oversample (×32) уже выставлен в BOARD_BootClockRUN().
|
||||
|
|
@ -88,7 +88,7 @@ extern "C"
|
|||
bsp_status_t bsp_mqs_init(void);
|
||||
|
||||
/**
|
||||
* @brief Деинициализация: остановить DMA, сбросить SAI1 и MQS.
|
||||
* @brief Деинициализация: остановить DMA, сбросить SAI3 и MQS.
|
||||
*
|
||||
* Безопасно вызывать даже если воспроизведение уже завершилось.
|
||||
* После вызова модуль требует повторного bsp_mqs_init().
|
||||
|
|
@ -152,7 +152,7 @@ extern "C"
|
|||
* ----------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Инициализация усилителя: PWM4 SM0, 16 кГц, duty 50%.
|
||||
* @brief Инициализация усилителя: PWM4 SM0, 12 кГц, duty 50%.
|
||||
*
|
||||
* Настраивает XBARA1 (fault disable), PWM4 submodule 0 channel A.
|
||||
* Вызывать до bsp_mqs_play() — без ШИМ на VOLUME усиление равно нулю.
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
/**
|
||||
* @file bsp_mqs.c
|
||||
* @brief BSP MQS: SAI1 TX + eDMA + MQS для MIMXRT1052CVJ5B.
|
||||
* @brief BSP MQS: SAI3 TX + eDMA + MQS для MIMXRT1052CVJ5B.
|
||||
*
|
||||
* Тактирование:
|
||||
* SAI1_CLK_ROOT = SysPLL × (18/27) / (SAI1_CLK_PRED+1=4) / (SAI1_CLK_PODF+1=2)
|
||||
* ≈ 63 529 411 Гц (BOARD_BOOTCLOCKRUN_SAI1_CLK_ROOT)
|
||||
* Audio PLL = 24 МГц × (30 + 66/625) = 722.534 МГц
|
||||
* SAI3_CLK_ROOT = Audio PLL / 8 / 8 = 11 289 600 Гц
|
||||
* (kCLOCK_Sai3Mux=2, Sai3PreDiv=7, Sai3Div=7,
|
||||
* BOARD_BOOTCLOCKRUN_SAI3_CLK_ROOT)
|
||||
* Bit clock = 44100 × 16 × 2 = 1 411 200 Гц
|
||||
* MCLK делитель = 63 529 411 / 1 411 200 ≈ 45.0 (погрешность ~0.5 %)
|
||||
* MCLK делитель = 11 289 600 / 1 411 200 = 8 (точно, без погрешности)
|
||||
*
|
||||
* MQS oversample = 32, уже выставлен в BOARD_BootClockRUN() через
|
||||
* IOMUXC_MQSConfig(IOMUXC_GPR, kIOMUXC_MqsPwmOverSampleRate32, 0).
|
||||
*
|
||||
* Пин: GPIO_AD_B0_04 → MQS_RIGHT — замультиплексирован в BOARD_InitPins().
|
||||
*
|
||||
* eDMA: DMA0 канал 0, DMAMUX source kDmaRequestMuxSai1Tx.
|
||||
* eDMA: DMA0 канал 0, DMAMUX source kDmaRequestMuxSai3Tx.
|
||||
* Канал 0 зарезервирован за bsp_mqs. Прочие модули — каналы 1+.
|
||||
*
|
||||
* SAI API (SDK 2.4.7 / fsl_sai.h, fsl_sai_edma.h 2.7.3):
|
||||
|
|
@ -47,17 +49,17 @@
|
|||
#define MQS_SAI_CLOCK_GATE kCLOCK_Sai3
|
||||
#define MQS_SAI_CLK_FREQ BOARD_BOOTCLOCKRUN_SAI3_CLK_ROOT
|
||||
|
||||
/** eDMA канал, выделенный под SAI1 TX. */
|
||||
/** eDMA канал, выделенный под SAI3 TX. */
|
||||
#define MQS_DMA_CHANNEL (0U)
|
||||
|
||||
/** DMAMUX запрос для SAI1 TX. */
|
||||
/** DMAMUX запрос для SAI3 TX. */
|
||||
#define MQS_DMAMUX_SOURCE kDmaRequestMuxSai3Tx
|
||||
|
||||
/** Приоритет прерывания DMA (ниже USB = 3, выше нормальных задач). */
|
||||
#define MQS_DMA_IRQ_PRIORITY (5U)
|
||||
#define MQS_HMCLK_GATE kCLOCK_Mqs
|
||||
/**
|
||||
* FIFO watermark — половина глубины FIFO SAI1.
|
||||
* FIFO watermark — половина глубины FIFO SAI3.
|
||||
* FSL_FEATURE_SAI_FIFO_COUNTn(x) принимает экземпляр SAI и возвращает
|
||||
* глубину FIFO в словах (32 для RT1052). Деление на 2 даёт оптимальную
|
||||
* латентность DMA: запрос формируется когда в FIFO остаётся место для
|
||||
|
|
@ -108,11 +110,6 @@ static void mqs_edma_callback(I2S_Type *p_base, sai_edma_handle_t *p_handle, sta
|
|||
/* --------------------------------------------------------------------------
|
||||
* Публичный API
|
||||
* ----------------------------------------------------------------------- */
|
||||
/*
|
||||
* AUDIO PLL setting: Frequency = Fref * (DIV_SELECT + NUM / DENOM)
|
||||
* = 24 * (32 + 768/1000)
|
||||
* = 786.432 MHz
|
||||
*/
|
||||
|
||||
bsp_status_t bsp_mqs_init(void)
|
||||
{
|
||||
|
|
@ -121,7 +118,7 @@ bsp_status_t bsp_mqs_init(void)
|
|||
return BSP_OK;
|
||||
}
|
||||
|
||||
/* --- Тактирование SAI1 --- */
|
||||
/* --- Тактирование SAI3 --- */
|
||||
CLOCK_EnableClock(MQS_SAI_CLOCK_GATE);
|
||||
|
||||
/* --- Тактирование MQS (CCGR0[CG2]) --- */
|
||||
|
|
@ -132,10 +129,10 @@ bsp_status_t bsp_mqs_init(void)
|
|||
IOMUXC_MQSEnterSoftwareReset(IOMUXC_GPR, false);
|
||||
IOMUXC_MQSEnable(IOMUXC_GPR, true);
|
||||
|
||||
/* --- SAI1: базовая инициализация (снимает reset, включает clock gate) --- */
|
||||
/* --- SAI3: базовая инициализация (снимает reset, включает clock gate) --- */
|
||||
SAI_Init(MQS_SAI_BASE);
|
||||
|
||||
/* --- SAI1 TX: классический I2S, master, 16 бит, стерео, канал 0. --- */
|
||||
/* --- SAI3 TX: классический I2S, master, 16 бит, стерео, канал 0. --- */
|
||||
sai_transceiver_t sai_cfg;
|
||||
|
||||
SAI_GetLeftJustifiedConfig(&sai_cfg, kSAI_WordWidth16bits, kSAI_Stereo,
|
||||
|
|
@ -159,7 +156,7 @@ bsp_status_t bsp_mqs_init(void)
|
|||
EDMA_Init(DMA0, &dma_cfg);
|
||||
EDMA_CreateHandle(&s_dma_handle, DMA0, MQS_DMA_CHANNEL);
|
||||
|
||||
/* --- DMAMUX: канал 0 → SAI1 TX --- */
|
||||
/* --- DMAMUX: канал 0 → SAI3 TX --- */
|
||||
DMAMUX_Init(DMAMUX);
|
||||
DMAMUX_SetSource(DMAMUX, MQS_DMA_CHANNEL, (uint8_t) MQS_DMAMUX_SOURCE);
|
||||
DMAMUX_EnableChannel(DMAMUX, MQS_DMA_CHANNEL);
|
||||
|
|
@ -188,7 +185,7 @@ void bsp_mqs_deinit(void)
|
|||
}
|
||||
|
||||
SAI_TransferTerminateSendEDMA(MQS_SAI_BASE, &s_sai_tx_handle);
|
||||
/* SAI_TxReset() — сброс logic + FIFO (аналог kSAI_ResetAll).
|
||||
/* SAI_TxReset() — сброс logic + FIFO (аналог kSAI_ResetAll) для SAI3.
|
||||
* SAI_TxSoftwareReset() отсутствует в данной версии SDK. */
|
||||
SAI_TxReset(MQS_SAI_BASE);
|
||||
IOMUXC_MQSEnable(IOMUXC_GPR, false);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
* аудио-сигнала на входе (MQS_RIGHT через RC-фильтр → SOUND_OUT).
|
||||
*
|
||||
* Управление громкостью:
|
||||
* PWM4 SM0 PWM_A, частота 16 кГц, центрально-симметричный режим.
|
||||
* PWM4 SM0 PWM_A, частота 12 кГц, центрально-симметричный режим.
|
||||
* duty 0% → DC_VOL ≈ 0 В → усиление минимально (тишина).
|
||||
* duty 50% → DC_VOL ≈ 2.5 В → номинальная громкость.
|
||||
* duty 100%→ DC_VOL ≈ 5 В → максимальное усиление.
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
* Тактирование:
|
||||
* IPG clock = AHB/4 = 600/4 = 150 МГц.
|
||||
* PWM prescaler = /16 → PWM clock = 9.375 МГц.
|
||||
* Fpwm = 16000 Гц (центрально-симметричный режим).
|
||||
* Fpwm = 9 375 000 / 586 / 2 = 12000 Гц (центрально-симметричный режим).
|
||||
*/
|
||||
|
||||
#include "bsp/mqs.h"
|
||||
|
|
@ -96,7 +96,7 @@ bsp_status_t bsp_mqs_amp_init(void)
|
|||
/* --- ForceSignal: использовать нормальный PWM-сигнал --- */
|
||||
PWM_SetupForceSignal(AMP_PWM_BASE, AMP_PWM_SUBMODULE, AMP_PWM_CHANNEL, kPWM_UsePwm);
|
||||
|
||||
/* --- PWM-сигнал: 16 кГц, центрально-симметричный, duty 50% --- */
|
||||
/* --- PWM-сигнал: 12 кГц, центрально-симметричный, duty 50% --- */
|
||||
const pwm_signal_param_t PWM_SIGNAL = {
|
||||
.pwmChannel = AMP_PWM_CHANNEL,
|
||||
.dutyCyclePercent = AMP_DEFAULT_DUTY,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
* Пин LOW (тока нет) → BSP_OPTO_STATE_INACTIVE
|
||||
*
|
||||
* Режимы каналов (bsp_opto_ch_mode_t):
|
||||
* ы
|
||||
* BSP_OPTO_MODE_LEVEL — IN1, IN2
|
||||
* Детектирование уровня с программным дебаунсом.
|
||||
* ISR переключает направление прерывания (RISING↔FALLING) после каждого фронта,
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ target_link_libraries(firmware_test PRIVATE bsp_provisioning)
|
|||
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
|
||||
| `bsp_board` | PRIVATE | Транзитивно: clock, SDK headers |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Особенности
|
||||
|
|
|
|||
|
|
@ -25,9 +25,8 @@
|
|||
* @brief Прочитать уникальный идентификатор чипа из OCOTP.
|
||||
*
|
||||
* Читает OCOTP_CFG0 (UID[31:0]) и OCOTP_CFG1 (UID[63:32]).
|
||||
* Результат записывается в big-endian порядке: p_uid[0] — старший байт CFG1,
|
||||
* p_uid[7] — младший байт CFG0. Hex-строка совпадает с визуальным порядком слов
|
||||
* в Reference Manual (MIMXRT1052RM Table 46-2).
|
||||
* Результат записывается в нативном порядке байт (little-endian на Cortex-M7):
|
||||
* p_uid[0..3] = CFG0 (UID[31:0]), p_uid[4..7] = CFG1 (UID[63:32]).
|
||||
*
|
||||
* Функция выполняет OCOTP_Init() и включает clock gate перед чтением.
|
||||
* Clock gate остаётся открытым после вызова (паттерн проекта).
|
||||
|
|
|
|||
|
|
@ -77,8 +77,9 @@ target_compile_definitions(firmware_test PRIVATE
|
|||
/* Инициализация — вызвать до bsp_tick_init() */
|
||||
bsp_status_t bsp_qspi_init(void);
|
||||
|
||||
/* Идентификация */
|
||||
/* Идентификация — bsp_qspi_read_jedec_id() работает и без успешного init() */
|
||||
bsp_status_t bsp_qspi_read_jedec_id(bsp_qspi_jedec_t *p_jedec);
|
||||
const char *bsp_qspi_decode_chip(uint8_t cap_byte, uint32_t *p_size_mb); /* device_id & 0xFF → "W25Q128" + МБ */
|
||||
uint32_t bsp_qspi_flash_size(void); /* доступно после init() */
|
||||
|
||||
/* Стирание */
|
||||
|
|
@ -117,9 +118,13 @@ board_hw_init();
|
|||
bsp_qspi_init(); /* ← до bsp_tick_init() */
|
||||
bsp_tick_init();
|
||||
|
||||
/* Идентификация чипа */
|
||||
/* Идентификация чипа — работает даже если bsp_qspi_init() выше вернула
|
||||
ошибку (LUT для JEDEC грузится безусловным первым шагом внутри неё) */
|
||||
bsp_qspi_jedec_t jedec;
|
||||
bsp_qspi_read_jedec_id(&jedec);
|
||||
uint32_t size_mb;
|
||||
const char *chip_name = bsp_qspi_decode_chip((uint8_t) (jedec.device_id & 0xFF), &size_mb);
|
||||
/* chip_name = "W25Q128", size_mb = 16 — либо "UNKNOWN"/0, если чип не опознан */
|
||||
|
||||
/* Стереть сектор и записать страницу */
|
||||
bsp_qspi_erase_sector(0x00010000);
|
||||
|
|
|
|||
|
|
@ -90,9 +90,29 @@ bsp_status_t bsp_qspi_init(void);
|
|||
*
|
||||
* @param[out] p_jedec Результат. Не NULL.
|
||||
* @retval BSP_OK / BSP_ERR.
|
||||
*
|
||||
* @note Работает и после проваленного bsp_qspi_init() — LUT-слот для чтения
|
||||
* JEDEC ID грузится безусловным первым шагом внутри него, до любой из
|
||||
* проверок, на которых init() мог отвалиться. Полезно для диагностики
|
||||
* "что именно распаяно", когда чип не опознан/не тот.
|
||||
*/
|
||||
bsp_status_t bsp_qspi_read_jedec_id(bsp_qspi_jedec_t *p_jedec);
|
||||
|
||||
/**
|
||||
* @brief Человекочитаемое имя и ёмкость чипа по capacity byte JEDEC ID.
|
||||
*
|
||||
* Тот же байт, что различает поддерживаемые чипы в bsp_qspi_init() — вынесен
|
||||
* отдельно, чтобы потребитель мог опознать чип из уже прочитанного
|
||||
* bsp_qspi_jedec_t.device_id, не завися от успеха bsp_qspi_init().
|
||||
*
|
||||
* @param[in] cap_byte Байт ёмкости (device_id & 0xFF).
|
||||
* @param[out] p_size_mb Ёмкость чипа, МБ. 0, если байт не распознан.
|
||||
* Может быть NULL, если размер не нужен.
|
||||
* @return "W25Q64"/"W25Q128"/"W25Q256"/"W25Q512", либо "UNKNOWN" для
|
||||
* нераспознанного байта.
|
||||
*/
|
||||
const char *bsp_qspi_decode_chip(uint8_t cap_byte, uint32_t *p_size_mb);
|
||||
|
||||
/**
|
||||
* @brief Стирание сектора 4 KB.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -115,8 +115,8 @@
|
|||
/**
|
||||
* @brief Размер читаемого буфера для однобайтных SR-команд.
|
||||
*
|
||||
* FlexSPI FIFO работает минимальными единицами в 4 байта (RXWMRK=0, 1 FILL
|
||||
* unit = 4 bytes). Читаем 4 байта, используем только byte[0].
|
||||
* FlexSPI FIFO работает минимальными единицами в 4 байта. Читаем 4 байта,
|
||||
* используем только byte[0].
|
||||
*/
|
||||
#define SR_READ_LEN 4U
|
||||
|
||||
|
|
@ -338,9 +338,9 @@ AT_QUICKACCESS_SECTION_CODE(static status_t qspi_read_tail(uint8_t *p_dst, uint3
|
|||
|
||||
while (!done)
|
||||
{
|
||||
const uint32_t FILL =
|
||||
const uint32_t FILL_UNITS =
|
||||
(QSPI_BASE->IPRXFSTS & FLEXSPI_IPRXFSTS_FILL_MASK) >> FLEXSPI_IPRXFSTS_FILL_SHIFT;
|
||||
if (FILL >= WORDS_NEEDED)
|
||||
if ((FILL_UNITS * QSPI_WM_UNIT_WORDS) >= WORDS_NEEDED)
|
||||
{
|
||||
done = true;
|
||||
}
|
||||
|
|
@ -508,7 +508,11 @@ AT_QUICKACCESS_SECTION_CODE(static void qspi_ip_setup(uint32_t seq_idx, uint32_t
|
|||
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_ip_read(uint32_t seq_idx, uint32_t addr,
|
||||
uint8_t *p_rx, uint32_t data_len))
|
||||
{
|
||||
qspi_ip_setup(seq_idx, addr, data_len);
|
||||
/* IDATSZ округляем вверх до кратного QSPI_RFDR_WORD_BYTES (4) */
|
||||
const uint32_t IDATSZ_ALIGNED =
|
||||
(data_len + (QSPI_RFDR_WORD_BYTES - 1U)) & ~(QSPI_RFDR_WORD_BYTES - 1U);
|
||||
|
||||
qspi_ip_setup(seq_idx, addr, IDATSZ_ALIGNED);
|
||||
const status_t RESULT = qspi_read_fifo(p_rx, data_len);
|
||||
qspi_wait_idle();
|
||||
return RESULT;
|
||||
|
|
@ -653,6 +657,42 @@ AT_QUICKACCESS_SECTION_CODE(static bsp_status_t qspi_detect_chip(uint8_t cap_byt
|
|||
return BSP_OK;
|
||||
}
|
||||
|
||||
const char *bsp_qspi_decode_chip(uint8_t cap_byte, uint32_t *p_size_mb)
|
||||
{
|
||||
const char *p_name;
|
||||
uint32_t size_mb;
|
||||
|
||||
switch (cap_byte)
|
||||
{
|
||||
case BSP_QSPI_CAP_64MBIT:
|
||||
p_name = "W25Q64";
|
||||
size_mb = 8U;
|
||||
break;
|
||||
case BSP_QSPI_CAP_128MBIT:
|
||||
p_name = "W25Q128";
|
||||
size_mb = 16U;
|
||||
break;
|
||||
case BSP_QSPI_CAP_256MBIT:
|
||||
p_name = "W25Q256";
|
||||
size_mb = 32U;
|
||||
break;
|
||||
case BSP_QSPI_CAP_512MBIT:
|
||||
p_name = "W25Q512";
|
||||
size_mb = 64U;
|
||||
break;
|
||||
default:
|
||||
p_name = "UNKNOWN";
|
||||
size_mb = 0U;
|
||||
break;
|
||||
}
|
||||
|
||||
if (p_size_mb != NULL)
|
||||
{
|
||||
*p_size_mb = size_mb;
|
||||
}
|
||||
return p_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Общая реализация erase-операций.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -26,9 +26,10 @@ bsp_status_t bsp_sd_init(void);
|
|||
bsp_status_t bsp_sd_deinit(void);
|
||||
|
||||
/*
|
||||
* Проверить физическое наличие карты через регистр USDHC PRSSTAT.
|
||||
* Не требует предварительного вызова bsp_sd_init().
|
||||
* Включает тактирование USDHC1 на время чтения регистра.
|
||||
* Проверить физическое наличие карты через USDHC PRES_STATE.CINST
|
||||
* (USDHC_GetPresentStatusFlags). Включает тактирование USDHC1 на время
|
||||
* чтения; не требует предварительного вызова bsp_sd_init(). Корректно
|
||||
* пока пин D13 замаплен на USDHC1_CD_B.
|
||||
*/
|
||||
bool bsp_sd_is_inserted(void);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,15 +5,11 @@
|
|||
#include "bsp/sd.h"
|
||||
|
||||
#include "fsl_sd.h"
|
||||
#include "fsl_usdhc.h" /* USDHC_Reset — аппаратный сброс FIFO/state machine */
|
||||
#include "sdmmc_config.h" /* BOARD_SD_Config, BOARD_SDMMC_SD_HOST_BASEADDR */
|
||||
#include "fsl_usdhc.h"
|
||||
#include "sdmmc_config.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <string.h> /* memset */
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Глобальный дескриптор карты — нужен SDK-стеку (передаётся по указателю
|
||||
* в BOARD_SD_Config и sd_disk_initialize через g_sd).
|
||||
* Объявлен без static — fsl_sd_disk.c ссылается на него как extern sd_card_t g_sd.
|
||||
* ------------------------------------------------------------------------- */
|
||||
extern sd_card_t g_sd;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
|
|
@ -37,7 +33,8 @@ static void ensure_host_configured(void)
|
|||
{
|
||||
return;
|
||||
}
|
||||
/* cd=NULL, userData=NULL: CD управляется хостом через PRSSTAT */
|
||||
/* cd=NULL, userData=NULL: детект — GPIO-callback внутри BOARD_SD_Config(),
|
||||
* не внешний callback сюда (см. bsp_sd_is_inserted() — тот же механизм). */
|
||||
BOARD_SD_Config(&g_sd, NULL, BOARD_SDMMC_SD_HOST_IRQ_PRIORITY, NULL);
|
||||
g_s_host_configured = true;
|
||||
}
|
||||
|
|
@ -54,19 +51,8 @@ bsp_status_t bsp_sd_init(void)
|
|||
}
|
||||
|
||||
/*
|
||||
* Аппаратный сброс USDHC FIFO + command/data state machine ПЕРЕД
|
||||
* повторной инициализацией. Без этого non-blocking host driver SDK
|
||||
* (fsl_sdmmc_host.c) может остаться в состоянии "ожидание завершения
|
||||
* предыдущей транзакции" после SD_HostDeinit() на прошлом прогоне —
|
||||
* физическая транзакция уже умерла вместе с deinit, но внутренний
|
||||
* флаг ожидания interrupt остаётся выставленным, и следующий f_mount()
|
||||
* блокируется навсегда в ожидании события, которое никогда не придёт.
|
||||
*
|
||||
* USDHC_Reset с маской kUSDHC_ResetAll сбрасывает контроллер на
|
||||
* регистровом уровне, не полагаясь на состояние, оставленное
|
||||
* предыдущей сессией. Безопасно вызывать даже при первом запуске —
|
||||
* базовый адрес уже доступен через BOARD_SDMMC_SD_HOST_BASEADDR
|
||||
* (clock на этот момент должен быть включён, см. ниже).
|
||||
* Аппаратный сброс USDHC FIFO + command/data state machine перед
|
||||
* повторной инициализацией.
|
||||
*/
|
||||
CLOCK_EnableClock(kCLOCK_Usdhc1); /* тактирование нужно ДО сброса регистров */
|
||||
USDHC_Reset(BOARD_SDMMC_SD_HOST_BASEADDR, kUSDHC_ResetAll, 100U);
|
||||
|
|
@ -80,12 +66,20 @@ bsp_status_t bsp_sd_init(void)
|
|||
(void) memset(&g_sd, 0, sizeof(g_sd));
|
||||
g_s_host_configured = false; /* форсируем повторный BOARD_SD_Config ниже */
|
||||
|
||||
ensure_host_configured(); /* только BOARD_SD_Config — заполняет g_sd */
|
||||
ensure_host_configured(); /* BOARD_SD_Config — заполняет g_sd, включая usrParam.pwr */
|
||||
|
||||
/*
|
||||
* Полный init (host + card) происходит в sd_disk_initialize → SD_Init,
|
||||
* который вызывается из f_mount → disk_initialize.
|
||||
*/
|
||||
if (SD_HostInit(&g_sd) != kStatus_Success)
|
||||
{
|
||||
return BSP_ERR_HW;
|
||||
}
|
||||
|
||||
if (SD_PollingCardInsert(&g_sd, kSD_Inserted) != kStatus_Success)
|
||||
{
|
||||
return BSP_ERR_HW;
|
||||
}
|
||||
|
||||
SD_SetCardPower(&g_sd, false);
|
||||
SD_SetCardPower(&g_sd, true);
|
||||
|
||||
g_s_initialized = true;
|
||||
return BSP_OK;
|
||||
|
|
@ -101,12 +95,6 @@ bsp_status_t bsp_sd_deinit(void)
|
|||
SD_HostDeinit(&g_sd);
|
||||
SD_SetCardPower(&g_sd, false);
|
||||
|
||||
/*
|
||||
* Дополнительный аппаратный сброс сразу после deinit — гарантирует,
|
||||
* что FIFO и state machine USDHC не останутся в промежуточном
|
||||
* состоянии независимо от того, что делает (или не делает)
|
||||
* SD_HostDeinit() из SDK на уровне регистров.
|
||||
*/
|
||||
USDHC_Reset(BOARD_SDMMC_SD_HOST_BASEADDR, kUSDHC_ResetAll, 100U);
|
||||
|
||||
g_s_initialized = false;
|
||||
|
|
@ -116,6 +104,8 @@ bsp_status_t bsp_sd_deinit(void)
|
|||
|
||||
bool bsp_sd_is_inserted(void)
|
||||
{
|
||||
return GPIO_PinRead(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN) ==
|
||||
BOARD_SDMMC_SD_CD_INSERT_LEVEL;
|
||||
CLOCK_EnableClock(kCLOCK_Usdhc1);
|
||||
|
||||
uint32_t ps = USDHC_GetPresentStatusFlags(BOARD_SDMMC_SD_HOST_BASEADDR);
|
||||
return (ps & kUSDHC_CardInsertedFlag) != 0U;
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
# bsp_sdram — внешняя SDRAM MT48LC16M16A2 (32 МБ)
|
||||
|
||||
Минимальная верификация доступности внешней SDRAM, подключённой к SEMC.
|
||||
Подробное тестирование (паттерны, шина адреса/данных, retention) выполняется
|
||||
в тест-модуле `firmware_test/test_sdram.c`, который использует константы
|
||||
и API этого модуля.
|
||||
Подъём SEMC (для прошивок без DCD) и минимальная верификация доступности
|
||||
внешней SDRAM, подключённой к SEMC. Подробное тестирование (паттерны, шина
|
||||
адреса/данных, retention) выполняется в тест-модуле `firmware_test/test_sdram.c`,
|
||||
который использует API этого модуля (путь с DCD, см. ниже).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -29,16 +29,45 @@
|
|||
Тестовая база смещена на 2 МБ от начала — гарантированно выше `.data`/`.bss`
|
||||
прошивки и ниже non-cacheable региона.
|
||||
|
||||
**Важно:** SEMC инициализируется через DCD **до вызова `main()`**. Этот модуль
|
||||
не настраивает SEMC и не трогает его регистры. Если DCD не отработал —
|
||||
`bsp_sdram_init()` вернёт ошибку, но исправить ситуацию из модуля нельзя.
|
||||
---
|
||||
|
||||
## Контракт: кто поднимает SEMC
|
||||
|
||||
Два независимых пути, в зависимости от того, есть ли у прошивки DCD:
|
||||
|
||||
- **С DCD** (`firmware_test`, `bsp_boot_xip`): SEMC поднят DCD **до вызова
|
||||
`main()`**. Этот модуль в этом случае регистры SEMC не трогает — только
|
||||
`bsp_sdram_init()` для верификации.
|
||||
- **Без DCD** (bootloader, `bsp_boot_xip_no_dcd`; в будущем `tft_app`): SEMC
|
||||
не поднимает никто, пока не будет явно вызван `bsp_sdram_configure()` —
|
||||
побитовый порт проверенной в производстве DCD-последовательности
|
||||
(`tools/host/dcd/dcd.bin`, «блок 2» — первый блок там мёртвый код,
|
||||
полностью перезаписывается вторым до какого-либо использования, поэтому
|
||||
не переносился). Источник истины — сам DCD, а не пересчёт по формулам SDK
|
||||
из наносекунд: значения регистров контроллера SEMC скопированы дословно.
|
||||
|
||||
**Почему `bsp_sdram_configure()` сама поднимает тактирование.** Штатный
|
||||
`BOARD_BootClockRUN()` (его вызывает `board_hw_init()` в каждой прошивке) НЕ
|
||||
настраивает PLL2 → PFD2 → делитель SEMC — этот блок в `clock_config.c`
|
||||
выключен макросом `SKIP_SYSCLK_INIT`, который определён для **всех** таргетов
|
||||
сборки (`bsp/CMakeLists.txt`), включая `bsp_boot_xip_no_dcd`. Смысл макроса —
|
||||
не глитчить PLL, пока на нём уже висит поднятая DCD SDRAM (случай
|
||||
`firmware_test`); но для пути без DCD это побочно означает, что тактирование
|
||||
SEMC не настраивает вообще никто, кроме `bsp_sdram_configure()`. Результат —
|
||||
SEMC ≈135.77 МГц (PLL2 528 МГц → PFD2 `FRAC=35` ≈271.54 МГц → `SEMC_PODF` ÷2),
|
||||
под эту частоту тюнингованы все timing-регистры DCD.
|
||||
|
||||
Вызывать `bsp_sdram_configure()` в пути с DCD не нужно и не имеет смысла —
|
||||
DCD уже сделала эту работу раньше, а `bsp_sdram_configure()` дублировала бы
|
||||
её же на живой памяти.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```c
|
||||
bsp_status_t bsp_sdram_init(void);
|
||||
bsp_status_t bsp_sdram_configure(void); /* поднять SEMC: тактирование → пины → контроллер → init-команды SDRAM */
|
||||
bsp_status_t bsp_sdram_init(void); /* верифицировать SDRAM (SEMC уже поднят — DCD либо bsp_sdram_configure()) */
|
||||
```
|
||||
|
||||
**Публичные константы:**
|
||||
|
|
@ -55,6 +84,37 @@ bsp_status_t bsp_sdram_init(void);
|
|||
Константы размеров — для потребителей; `bsp_sdram` не запускает по ним
|
||||
внутренних проходов.
|
||||
|
||||
**Поведение `bsp_sdram_configure()`:**
|
||||
|
||||
1. Тактирование: `CLOCK_InitSysPll()` (PLL2, 528 МГц) → `CLOCK_InitSysPfd()`
|
||||
(PFD2, `FRAC=35`) → `CLOCK_SetMux`/`CLOCK_SetDiv` (SEMC ← alt ← PFD2, ÷2).
|
||||
2. IOMUX: пины `GPIO_EMC_00..41` → ALT0 (функция SEMC), `SW_PAD_CTL_PAD` =
|
||||
`0x000110F9`; `GPIO_EMC_39` (`SEMC_DQS`) дополнительно получает `SION`
|
||||
(иначе SEMC не сможет читать собственный read-strobe).
|
||||
3. Регистры контроллера SEMC (`MCR`, `BMCR0/1`, `BR[0..8]`, `IOCR`,
|
||||
`SDRAMCR0..3`, `DBICR0/1`, `IPCR1/2`) — побитово из DCD.
|
||||
4. Командная последовательность SDRAM через `SEMC_SendIPCommand()`:
|
||||
precharge-all → 2×auto-refresh → mode-set (`0x33` = BL8/sequential/CL3,
|
||||
согласуется с `SDRAMCR0` и даташитом MT48LC16M16A2) → включение
|
||||
авто-refresh (`SDRAMCR3 = 0x50210A09`).
|
||||
5. AXI-QoS приоритеты SDRAM-мастеров (`0x41044100/104` LCD, `0x41442100/104`
|
||||
Cortex-M7 read/write_qos) — хвост DCD. Это не SEMC и не в заголовках
|
||||
`sdk/devices/MIMXRT1052` (NXP не заворачивает ARM'овский NIC-301 IP в
|
||||
CMSIS-структуру), но регистры реальные и документированы (i.MX RT1050 RM,
|
||||
гл. 29 "Network Interconnect Bus System (NIC-301)" — адреса сверены день-в-
|
||||
день: `0x41044100` = `GPV0_BASE(0x41000000)+0x44000+0x100` =
|
||||
`SIM_MAIN.LCD.read_qos`). Для bootloader инертны (нет конкуренции LCD/DMA
|
||||
vs CPU за шину), но входят в проверенную последовательность и понадобятся
|
||||
`tft_app`.
|
||||
|
||||
**Требует MPU Region 11** (`board_mpu_init()`, `bsp/generated/board.c`,
|
||||
`0x41000000`, 8 МБ) — штатный Region 10 (периферия, только 4 МБ от
|
||||
`0x40000000` = 4 домена AIPSTZ) NIC-301 GPV не покрывает; без Region 11 шаг 5
|
||||
фолтит (deny-all errata-регион 0 перехватывает всё, что не покрыто более
|
||||
специфичным регионом — `PRIVDEFENA` тут не спасает, т.к. регион 0 покрывает
|
||||
весь диапазон 0x0..0xFFFFFFFF и потому всегда matched). DCD это переживает —
|
||||
ROM пишет регистры до включения MPU. Уже добавлен в `board_mpu_init()`.
|
||||
|
||||
**Поведение `bsp_sdram_init()`:**
|
||||
|
||||
1. Ждёт перехода SEMC в IDLE (`SEMC->STS0 & SEMC_STS0_IDLE_MASK`),
|
||||
|
|
@ -66,11 +126,13 @@ bsp_status_t bsp_sdram_init(void);
|
|||
|
||||
**Коды возврата:**
|
||||
|
||||
| Код | Условие |
|
||||
| ----------------- | ------------------------------------------- |
|
||||
| `BSP_OK` | SDRAM доступна, оба паттерна совпали |
|
||||
| `BSP_ERR_TIMEOUT` | SEMC не перешёл в IDLE за 10 мс |
|
||||
| `BSP_ERR_INIT` | Хотя бы один паттерн не совпал при readback |
|
||||
| Функция | Код | Условие |
|
||||
| -------------------------- | ----------------- | -------------------------------------------- |
|
||||
| `bsp_sdram_configure()` | `BSP_OK` | Тактирование/пины/регистры/команды прошли |
|
||||
| `bsp_sdram_configure()` | `BSP_ERR_INIT` | IP-команда SEMC вернула ошибку (precharge/refresh/mode-set) |
|
||||
| `bsp_sdram_init()` | `BSP_OK` | SDRAM доступна, оба паттерна совпали |
|
||||
| `bsp_sdram_init()` | `BSP_ERR_TIMEOUT` | SEMC не перешёл в IDLE за 10 мс |
|
||||
| `bsp_sdram_init()` | `BSP_ERR_INIT` | Хотя бы один паттерн не совпал при readback |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -79,10 +141,19 @@ bsp_status_t bsp_sdram_init(void);
|
|||
```c
|
||||
#include "bsp/sdram.h"
|
||||
|
||||
/* Путь С DCD (firmware_test) — SEMC уже поднят до main(): */
|
||||
if (bsp_sdram_init() != BSP_OK) {
|
||||
handle_critical_error();
|
||||
}
|
||||
|
||||
/* Путь БЕЗ DCD (bootloader smoke-test и т.п.) — сначала поднять SEMC сами: */
|
||||
if (bsp_sdram_configure() != BSP_OK) {
|
||||
handle_semc_bringup_error();
|
||||
}
|
||||
if (bsp_sdram_init() != BSP_OK) {
|
||||
handle_sdram_error();
|
||||
}
|
||||
|
||||
/* Работа с памятью по адресам внутри
|
||||
[BSP_SDRAM_BASE_ADDR, BSP_SDRAM_BASE_ADDR + BSP_SDRAM_SIZE_BYTES) */
|
||||
```
|
||||
|
|
@ -91,15 +162,19 @@ if (bsp_sdram_init() != BSP_OK) {
|
|||
|
||||
## CMake
|
||||
|
||||
Сегодня линкует только `firmware_test` (путь с DCD). Для пути без DCD
|
||||
потребитель (например, bootloader) добавляет зависимость сам — модуль
|
||||
никого за собой не тянет:
|
||||
|
||||
```cmake
|
||||
target_link_libraries(firmware_test PRIVATE bsp_sdram)
|
||||
```
|
||||
|
||||
**Зависимости модуля:**
|
||||
|
||||
| Зависимость | Тип | Описание |
|
||||
| ------------ | ------- | -------------------------------------------------- |
|
||||
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
|
||||
| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаута SEMC IDLE |
|
||||
| `sdk_semc` | PRIVATE | `fsl_semc.h` — `SEMC->STS0`, `SEMC_STS0_IDLE_MASK` |
|
||||
| `bsp_board` | PRIVATE | Общие board-уровневые символы |
|
||||
| Зависимость | Тип | Описание |
|
||||
| ------------ | ------- | ----------------------------------------------------------------|
|
||||
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
|
||||
| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаута SEMC IDLE |
|
||||
| `sdk_semc` | PRIVATE | `fsl_semc.h` — регистры `SEMC->`, `SEMC_SendIPCommand()` |
|
||||
| `bsp_board` | PRIVATE | Транзитивно даёт `sdk_clock` (`CLOCK_Init*()`) и `sdk_device` (`IOMUXC`) — отдельных PRIVATE-строк на них не заводили |
|
||||
|
|
|
|||
|
|
@ -2,9 +2,15 @@
|
|||
* @file sdram.h
|
||||
* @brief BSP для внешней SDRAM MT48LC16M16A2 (32 МБ, шина 16 бит).
|
||||
*
|
||||
* Архитектурное ограничение [DECISION]:
|
||||
* SEMC инициализируется DCD до вызова main(). Этот модуль не трогает
|
||||
* регистры SEMC — только верифицирует работоспособность памяти.
|
||||
* Два пути в зависимости от того, кто использует SEMC:
|
||||
*
|
||||
* - Прошивки С DCD (firmware_test): SEMC поднят DCD до main().
|
||||
* bsp_sdram_init() только верифицирует доступность памяти.
|
||||
*
|
||||
* - Прошивки БЕЗ DCD (bootloader smoke-test; в будущем — tft_app под свой
|
||||
* XIP): bsp_sdram_configure() сам поднимает SEMC (порт проверенной
|
||||
* DCD-последовательности в C, см. tools/host/dcd/dcd.bin), затем
|
||||
* bsp_sdram_init() верифицирует, как и в первом случае.
|
||||
*
|
||||
* Карта памяти:
|
||||
* 0x80000000 — начало SDRAM (SEMC BR0)
|
||||
|
|
@ -14,16 +20,10 @@
|
|||
* Тестовый регион (не пересекается с .data/.bss и non-cacheable):
|
||||
* 0x80200000 — начало (2 MB offset от базы)
|
||||
*
|
||||
* Использование:
|
||||
* Использование (путь без DCD):
|
||||
* @code
|
||||
* bsp_sdram_result_t result;
|
||||
*
|
||||
* if (bsp_sdram_init() != BSP_OK) { // DCD не отработал
|
||||
* handle_critical_error();
|
||||
* }
|
||||
*
|
||||
* bsp_sdram_test_fast(&result); // ~50 мс, 64 KB
|
||||
* bsp_sdram_test_full(&result); // ~2–5 с, 1 MB
|
||||
* if (bsp_sdram_configure() != BSP_OK) { handle_semc_bringup_error(); }
|
||||
* if (bsp_sdram_init() != BSP_OK) { handle_sdram_error(); }
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
|
|
@ -65,17 +65,51 @@
|
|||
|
||||
/* ── Public API ────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @brief Поднять SEMC и инициализировать внешнюю SDRAM (для вызывателей без DCD).
|
||||
*
|
||||
* Побитово-точный порт проверенной в производстве DCD-последовательности
|
||||
* (tools/host/dcd/dcd.bin, «блок 2») в вызываемый C-код:
|
||||
* 1. тактирование SEMC (PLL2 528 МГц → PFD2 271.54 МГц → ÷2 = 135.77 МГц);
|
||||
* 2. IOMUX/PAD пинов GPIO_EMC (функция SEMC, DQS с SION);
|
||||
* 3. регистры контроллера SEMC (MCR/BR/IOCR/SDRAMCR0..3/…), значения из DCD;
|
||||
* 4. командная последовательность SDRAM: precharge-all → 2×auto-refresh →
|
||||
* mode-set → включение авто-refresh;
|
||||
* 5. AXI-QoS приоритеты SDRAM-мастеров (LCD/Cortex-M7) — NIC-301 GPV,
|
||||
* отдельный IP вне карты SEMC (i.MX RT1050 RM, гл. 29).
|
||||
*
|
||||
* Прошивки С DCD (firmware_test) получают то же самое до main() и эту функцию
|
||||
* НЕ вызывают — только bsp_sdram_init().
|
||||
*
|
||||
* @note Безопасно вызывать в рантайме после board_hw_init(): CPU тактируется от
|
||||
* ARM PLL, FlexSPI-XIP — от USB1 PLL; PLL2/PFD2 поднимаются с нуля и не
|
||||
* задевают ни то, ни другое (под SKIP_SYSCLK_INIT штатный
|
||||
* BOARD_BootClockRUN() эту цепочку не трогает).
|
||||
*
|
||||
* @note Требует MPU Region 11 (board_mpu_init(), bsp/generated/board.c) —
|
||||
* шаг 5 пишет NIC-301 GPV (0x41000000+), который Region 10
|
||||
* (периферия, только 4 МБ от 0x40000000) не покрывает; без Region 11
|
||||
* запись фолтит (deny-all errata-регион 0 перехватывает всё
|
||||
* непокрытое). Уже добавлен — заметка для будущих правок MPU-таблицы.
|
||||
*
|
||||
* @retval BSP_OK SEMC поднят, SDRAM инициализирована.
|
||||
* @retval BSP_ERR_INIT Командная последовательность SDRAM не завершилась
|
||||
* (IP-команда SEMC вернула ошибку).
|
||||
*/
|
||||
bsp_status_t bsp_sdram_configure(void);
|
||||
|
||||
/**
|
||||
* @brief Верифицировать доступность SDRAM.
|
||||
*
|
||||
* Проверяет что SEMC контроллер инициализирован DCD и SDRAM отвечает —
|
||||
* выполняет минимальный write/read/verify на первых 4 байтах тестового
|
||||
* региона. Не затрагивает .data/.bss прошивки.
|
||||
* Проверяет что SEMC контроллер инициализирован (DCD или bsp_sdram_configure())
|
||||
* и SDRAM отвечает — выполняет минимальный write/read/verify на первых 4 байтах
|
||||
* тестового региона. Не затрагивает .data/.bss прошивки.
|
||||
*
|
||||
* @note Не реинициализирует SEMC — DCD уже сделал это до main().
|
||||
* @note Не (ре)инициализирует SEMC — предполагает, что DCD либо
|
||||
* bsp_sdram_configure() уже это сделали.
|
||||
*
|
||||
* @retval BSP_OK SDRAM доступна и отвечает корректно.
|
||||
* @retval BSP_ERR_INIT SEMC не готов (DCD не отработал).
|
||||
* @retval BSP_ERR_INIT SEMC не готов (инициализация не отработала).
|
||||
* @retval BSP_ERR_TIMEOUT SEMC занят дольше ожидаемого.
|
||||
*/
|
||||
bsp_status_t bsp_sdram_init(void);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
/**
|
||||
* @file sdram.c
|
||||
* @brief Верификация внешней SDRAM MT48LC16M16A2 (32 МБ, шина 16 бит).
|
||||
* @brief Подъём SEMC и верификация внешней SDRAM MT48LC16M16A2 (32 МБ, 16 бит).
|
||||
*
|
||||
* SEMC инициализируется DCD до main() — этот модуль только проверяет
|
||||
* доступность памяти. Регистры SEMC не модифицируются.
|
||||
* Два независимых куска:
|
||||
*
|
||||
* bsp_sdram_configure() — поднимает SEMC. Побитовый порт проверенной DCD-последовательности
|
||||
* (tools/host/dcd/dcd.bin)
|
||||
*
|
||||
* bsp_sdram_init() — верификация уже поднятой памяти (DCD или configure).
|
||||
* Регистры SEMC не модифицирует.
|
||||
*
|
||||
* Кэш: SDRAM настроена как Normal Write-Back cacheable (MPU Region 8).
|
||||
* Верификация требует явного cache maintenance перед readback — иначе
|
||||
|
|
@ -13,12 +18,13 @@
|
|||
#include "bsp/sdram.h"
|
||||
|
||||
#include "bsp/tick.h"
|
||||
#include "fsl_clock.h"
|
||||
#include "fsl_semc.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ── Константы ─────────────────────────────────────────────────────────── */
|
||||
/* ── Константы верификации ─────────────────────────────────────────────── */
|
||||
|
||||
/** @brief Таймаут ожидания готовности SEMC контроллера, мс. */
|
||||
#define SDRAM_SEMC_IDLE_TIMEOUT_MS 10U
|
||||
|
|
@ -38,11 +44,202 @@
|
|||
/** @brief Инверсия эталонного паттерна. */
|
||||
#define SDRAM_VERIFY_PATTERN_B 0x5A5A5A5AUL
|
||||
|
||||
/* ── Константы SEMC (значения из DCD, см. tools/host/dcd/dcd.bin) */
|
||||
|
||||
/** @brief PFD2 FRAC из DCD (PFD_528=0x00230000): 528 МГц ×18/35 ≈ 271.54 МГц. */
|
||||
#define SDRAM_SEMC_PFD2_FRAC 35U
|
||||
|
||||
/**
|
||||
* @brief Индекс пина GPIO_EMC_39 (SEMC_DQS) в массивах IOMUXC.
|
||||
*
|
||||
* SW_MUX_CTL_PAD[]/SW_PAD_CTL_PAD[] начинаются с GPIO_EMC_00 (индекс 0), шаг 1.
|
||||
* DQS — единственный пин, которому DCD ставит SION (вход strobe должен быть
|
||||
* принудительно включён, чтобы SEMC читал собственный строб).
|
||||
*/
|
||||
#define SDRAM_EMC_PAD_FIRST 0U
|
||||
#define SDRAM_EMC_PAD_LAST 41U /* GPIO_EMC_41 — последний EMC-пин */
|
||||
#define SDRAM_DQS_PAD_INDEX 39U /* GPIO_EMC_39 = SEMC_DQS */
|
||||
|
||||
/** @brief SION (Software Input On), бит 4 SW_MUX_CTL_PAD — только для DQS. */
|
||||
#define SDRAM_MUX_SION 0x00000010UL
|
||||
|
||||
/** @brief SW_PAD_CTL для всех EMC-пинов (DCD: 0x000110F9 на каждый). */
|
||||
#define SDRAM_PAD_CTL_VALUE 0x000110F9UL
|
||||
|
||||
/** @brief Адрес SDRAM для IP-команд SEMC (совпадает с BR0 = базой SDRAM). */
|
||||
#define SDRAM_SEMC_IPCMD_ADDR 0x80000000UL
|
||||
|
||||
/**
|
||||
* @brief Значение mode-register SDRAM (DCD IPTXDAT=0x33).
|
||||
*
|
||||
* M[2:0]=011 (burst length 8), M3=0 (sequential), M[6:4]=011 (CAS latency 3) —
|
||||
* согласуется с SDRAMCR0 (BL8/CL3) и даташитом MT48LC16M16A2.
|
||||
*/
|
||||
#define SDRAM_SEMC_MODE_REG 0x00000033UL
|
||||
|
||||
/*
|
||||
* AXI-QoS регистры арбитража доступа к SDRAM — NIC-301 GPV (Global
|
||||
* Programmer's View). Не влияют на корректность самой SDRAM — только на арбитраж при
|
||||
* конкуренции за шину (LCD/CPU vs SDRAM). */
|
||||
#define SDRAM_QOS_LCD_READ (*(volatile uint32_t *) 0x41044100UL)
|
||||
#define SDRAM_QOS_LCD_WRITE (*(volatile uint32_t *) 0x41044104UL)
|
||||
#define SDRAM_QOS_M7_READ (*(volatile uint32_t *) 0x41442100UL)
|
||||
#define SDRAM_QOS_M7_WRITE (*(volatile uint32_t *) 0x41442104UL)
|
||||
|
||||
/* ── Состояние модуля ──────────────────────────────────────────────────── */
|
||||
|
||||
static bool s_initialised = false;
|
||||
static bool g_s_initialised = false;
|
||||
|
||||
/* ── Внутренние функции ────────────────────────────────────────────────── */
|
||||
/**
|
||||
* @brief Поднять тактовую цепочку SEMC (порт секции «Clock Init» DCD).
|
||||
*
|
||||
*/
|
||||
static void sdram_configure_clock(void)
|
||||
{
|
||||
/* DCD PLL_SYS=0x00002001 → Fout = 24 МГц × (20 + 2×loopDivider + num/denom)
|
||||
* = 24 × (20 + 2 + 0) = 528 МГц. */
|
||||
static const clock_sys_pll_config_t SYS_PLL = {
|
||||
.loopDivider = 1, .numerator = 0, .denominator = 1, .src = 0, /* 24 МГц OSC */
|
||||
};
|
||||
|
||||
/* Гейтим SEMC на время переключения его источника (идиома clock_config.c). */
|
||||
CLOCK_DisableClock(kCLOCK_Semc);
|
||||
|
||||
CLOCK_InitSysPll(&SYS_PLL);
|
||||
CLOCK_InitSysPfd(kCLOCK_Pfd2, SDRAM_SEMC_PFD2_FRAC);
|
||||
|
||||
/* DCD CBCDR=0x00010D40 → SEMC_CLK_SEL=1 (alt), SEMC_ALT_CLK_SEL=0 (→PFD2),
|
||||
* SEMC_PODF=1 (÷2). */
|
||||
CLOCK_SetMux(kCLOCK_SemcAltMux, 0); /* alt = PLL2 PFD2 */
|
||||
CLOCK_SetMux(kCLOCK_SemcMux, 1); /* SEMC clock = alt (PFD2), не periph_clk */
|
||||
CLOCK_SetDiv(kCLOCK_SemcDiv, kCLOCK_SemcDivBy2);
|
||||
|
||||
CLOCK_EnableClock(kCLOCK_Semc);
|
||||
}
|
||||
|
||||
/* ── SEMC: пины ─────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @brief Замуксить пины GPIO_EMC на функцию SEMC + PAD-настройки (порт DCD).
|
||||
*
|
||||
*/
|
||||
static void sdram_configure_pins(void)
|
||||
{
|
||||
for (uint32_t i = SDRAM_EMC_PAD_FIRST; i <= SDRAM_EMC_PAD_LAST; i++)
|
||||
{
|
||||
IOMUXC->SW_MUX_CTL_PAD[i] = 0UL; /* ALT0 = функция SEMC */
|
||||
IOMUXC->SW_PAD_CTL_PAD[i] = SDRAM_PAD_CTL_VALUE;
|
||||
}
|
||||
|
||||
/* DQS: ALT0 + SION (DCD пишет сюда 0x10 вместо 0x00). */
|
||||
IOMUXC->SW_MUX_CTL_PAD[SDRAM_DQS_PAD_INDEX] = SDRAM_MUX_SION;
|
||||
}
|
||||
|
||||
/* ── SEMC: регистры контроллера ─────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @brief Записать регистры контроллера SEMC (дословно «блок 2» DCD).
|
||||
*
|
||||
*/
|
||||
static void sdram_configure_controller(void)
|
||||
{
|
||||
SEMC->MCR = 0x10000004UL; /* модуль вкл (MDIS=0), DQSMD=1 (DQS с пина), BTO=16 */
|
||||
|
||||
SEMC->BMCR0 = 0x00000081UL; /* веса AXI-очереди A */
|
||||
SEMC->BMCR1 = 0x00000081UL; /* веса AXI-очереди B */
|
||||
|
||||
/* Базовые регистры регионов SEMC. BR0 — сама SDRAM (0x80000000, 32 МБ,
|
||||
* VLD=1). BR1..BR8 — прочие регионы из проверенного DCD, переносятся как
|
||||
* есть (bootloader их не использует, но конфиг источника истины не режем). */
|
||||
SEMC->BR[0] = 0x8000001BUL;
|
||||
SEMC->BR[1] = 0x8200001BUL;
|
||||
SEMC->BR[2] = 0x8400001BUL;
|
||||
SEMC->BR[3] = 0x8600001BUL;
|
||||
SEMC->BR[4] = 0x90000021UL;
|
||||
SEMC->BR[5] = 0xA0000019UL;
|
||||
SEMC->BR[6] = 0xA8000017UL;
|
||||
SEMC->BR[7] = 0xA900001BUL;
|
||||
SEMC->BR[8] = 0x00000021UL;
|
||||
|
||||
SEMC->IOCR = 0x000079A8UL; /* внутренний pinmux SEMC */
|
||||
|
||||
/* Геометрия и тайминги SDRAM. SDRAMCR0: PS=16бит, BL=8, COL=9бит, CL=3 —
|
||||
* MT48LC16M16A2 (даташит: 512 колонок = 9 адресных бит). */
|
||||
SEMC->SDRAMCR0 = 0x00000F31UL;
|
||||
SEMC->SDRAMCR1 = 0x00652922UL;
|
||||
SEMC->SDRAMCR2 = 0x00020201UL;
|
||||
SEMC->SDRAMCR3 = 0x08193D0FUL; /* тайминги refresh; REN включим в конце */
|
||||
|
||||
/* DBICR0/DBICR1 — DCD их пишет, хотя DBI-устройства на плате нет и
|
||||
* SEMC_ConfigureSDRAM() их не трогает. Инертны (регион DBI не включён),
|
||||
* но переносятся дословно ради полного соответствия проверенному DCD. */
|
||||
SEMC->DBICR0 = 0x00000021UL;
|
||||
SEMC->DBICR1 = 0x00888888UL;
|
||||
|
||||
/* Параметры IP-команд: DATSZ=2 байта (запись mode-register 16-бит шиной). */
|
||||
SEMC->IPCR1 = 0x00000002UL;
|
||||
SEMC->IPCR2 = 0x00000000UL;
|
||||
}
|
||||
|
||||
/* ── SEMC: командная последовательность инициализации SDRAM ──────── */
|
||||
|
||||
/**
|
||||
* @brief Прогнать init-последовательность SDRAM через IP-команды SEMC.
|
||||
*
|
||||
* precharge-all → 2×auto-refresh → mode-set → включение авто-refresh.
|
||||
*
|
||||
* @retval BSP_OK все команды завершились успешно.
|
||||
* @retval BSP_ERR_INIT IP-команда SEMC вернула ошибку.
|
||||
*/
|
||||
static bsp_status_t sdram_issue_init_sequence(void)
|
||||
{
|
||||
const uint32_t ADDR = SDRAM_SEMC_IPCMD_ADDR;
|
||||
|
||||
if (SEMC_SendIPCommand(SEMC, kSEMC_MemType_SDRAM, ADDR, (uint32_t) kSEMC_SDRAMCM_Prechargeall,
|
||||
0, NULL) != kStatus_Success)
|
||||
{
|
||||
return BSP_ERR_INIT;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0U; i < 2U; i++)
|
||||
{
|
||||
if (SEMC_SendIPCommand(SEMC, kSEMC_MemType_SDRAM, ADDR,
|
||||
(uint32_t) kSEMC_SDRAMCM_AutoRefresh, 0, NULL) != kStatus_Success)
|
||||
{
|
||||
return BSP_ERR_INIT;
|
||||
}
|
||||
}
|
||||
|
||||
if (SEMC_SendIPCommand(SEMC, kSEMC_MemType_SDRAM, ADDR, (uint32_t) kSEMC_SDRAMCM_Modeset,
|
||||
SDRAM_SEMC_MODE_REG, NULL) != kStatus_Success)
|
||||
{
|
||||
return BSP_ERR_INIT;
|
||||
}
|
||||
|
||||
/* Включить авто-refresh + перейти на рабочие параметры refresh. DCD пишет
|
||||
* SDRAMCR3 повторно другим значением (не только бит REN) — переносим как
|
||||
* есть; это финальный «operational» refresh-конфиг после инициализации. */
|
||||
SEMC->SDRAMCR3 = 0x50210A09UL;
|
||||
|
||||
return BSP_OK;
|
||||
}
|
||||
|
||||
/* ── SEMC: AXI-QoS арбитраж ─ */
|
||||
|
||||
/**
|
||||
* @brief Настроить приоритеты доступа мастеров к SDRAM
|
||||
*
|
||||
* Требуют board_mpu_init() Region 11
|
||||
*/
|
||||
static void sdram_configure_axi_qos(void)
|
||||
{
|
||||
SDRAM_QOS_LCD_READ = 6UL;
|
||||
SDRAM_QOS_LCD_WRITE = 6UL;
|
||||
SDRAM_QOS_M7_READ = 7UL;
|
||||
SDRAM_QOS_M7_WRITE = 7UL;
|
||||
}
|
||||
|
||||
/* ── Внутренние функции верификации ────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @brief Дождаться перехода SEMC в состояние IDLE.
|
||||
|
|
@ -72,10 +269,10 @@ static bsp_status_t wait_semc_idle(void)
|
|||
*/
|
||||
static void flush_cache_at_test_base(void)
|
||||
{
|
||||
uint32_t *const p_addr = (uint32_t *) BSP_SDRAM_TEST_BASE_ADDR;
|
||||
uint32_t *const P_ADDR = (uint32_t *) BSP_SDRAM_TEST_BASE_ADDR;
|
||||
|
||||
SCB_CleanDCache_by_Addr(p_addr, (int32_t) SDRAM_CACHE_LINE_BYTES);
|
||||
SCB_InvalidateDCache_by_Addr(p_addr, (int32_t) SDRAM_CACHE_LINE_BYTES);
|
||||
SCB_CleanDCache_by_Addr(P_ADDR, (int32_t) SDRAM_CACHE_LINE_BYTES);
|
||||
SCB_InvalidateDCache_by_Addr(P_ADDR, (int32_t) SDRAM_CACHE_LINE_BYTES);
|
||||
__DSB();
|
||||
}
|
||||
|
||||
|
|
@ -87,16 +284,33 @@ static void flush_cache_at_test_base(void)
|
|||
*/
|
||||
static bsp_status_t verify_word(uint32_t pattern)
|
||||
{
|
||||
volatile uint32_t *const p_test = (volatile uint32_t *) BSP_SDRAM_TEST_BASE_ADDR;
|
||||
volatile uint32_t *const P_TEST = (volatile uint32_t *) BSP_SDRAM_TEST_BASE_ADDR;
|
||||
|
||||
*p_test = pattern;
|
||||
*P_TEST = pattern;
|
||||
flush_cache_at_test_base();
|
||||
|
||||
return (*p_test == pattern) ? BSP_OK : BSP_ERR_INIT;
|
||||
return (*P_TEST == pattern) ? BSP_OK : BSP_ERR_INIT;
|
||||
}
|
||||
|
||||
/* ── Public API ────────────────────────────────────────────────────────── */
|
||||
|
||||
bsp_status_t bsp_sdram_configure(void)
|
||||
{
|
||||
sdram_configure_clock();
|
||||
sdram_configure_pins();
|
||||
sdram_configure_controller();
|
||||
|
||||
bsp_status_t status = sdram_issue_init_sequence();
|
||||
if (status != BSP_OK)
|
||||
{
|
||||
return status;
|
||||
}
|
||||
#if defined(__NIC301_EXPERIMENTS_)
|
||||
sdram_configure_axi_qos();
|
||||
#endif
|
||||
return BSP_OK;
|
||||
}
|
||||
|
||||
bsp_status_t bsp_sdram_init(void)
|
||||
{
|
||||
bsp_status_t status = wait_semc_idle();
|
||||
|
|
@ -117,6 +331,6 @@ bsp_status_t bsp_sdram_init(void)
|
|||
return BSP_ERR_INIT;
|
||||
}
|
||||
|
||||
s_initialised = true;
|
||||
g_s_initialised = true;
|
||||
return BSP_OK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,12 +44,16 @@ flowchart TD
|
|||
|
||||
```c
|
||||
bsp_status_t bsp_uart_host_init(uint32_t baud);
|
||||
void bsp_uart_host_deinit(void);
|
||||
|
||||
bsp_status_t bsp_uart_host_write(const uint8_t *p_data, size_t len);
|
||||
bsp_status_t bsp_uart_host_write_str(const char *p_str);
|
||||
|
||||
size_t bsp_uart_host_read(uint8_t *p_buf, size_t len, uint32_t timeout_ms);
|
||||
int32_t bsp_uart_host_read_byte(uint32_t timeout_ms);
|
||||
|
||||
size_t bsp_uart_host_rx_available(void); /* байт в RX-буфере прямо сейчас */
|
||||
void bsp_uart_host_rx_flush(void); /* сбросить содержимое RX-буфера */
|
||||
```
|
||||
|
||||
`bsp_uart_host_read()` возвращает фактически прочитанное количество байт —
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Константы */
|
||||
/* -------------------------------------------------s------------------------- */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/** Передать в timeout_ms чтобы ждать данные бесконечно. */
|
||||
#define BSP_UART_HOST_WAIT_FOREVER (UINT32_MAX)
|
||||
|
|
|
|||
|
|
@ -84,17 +84,14 @@ bsp_status_t bsp_uart_host_init(uint32_t baud_rate)
|
|||
return BSP_ERR_INIT;
|
||||
}
|
||||
|
||||
/* Инициализация кольцевого буфера. */
|
||||
if (!ring_buffer_init(&g_s_rx_ring, g_s_rx_buf, BSP_UART_HOST_RX_BUFFER_SIZE))
|
||||
{
|
||||
/* Размер не степень двойки — ошибка конфигурации. */
|
||||
return BSP_ERR_INIT;
|
||||
}
|
||||
|
||||
/* Тактирование LPUART1. */
|
||||
CLOCK_EnableClock(kCLOCK_Lpuart1);
|
||||
|
||||
/* Настройка периферии. */
|
||||
lpuart_config_t config;
|
||||
LPUART_GetDefaultConfig(&config);
|
||||
config.baudRate_Bps = baud_rate;
|
||||
|
|
@ -191,7 +188,6 @@ size_t bsp_uart_host_read(uint8_t *p_buf, size_t len, uint32_t timeout_ms)
|
|||
continue;
|
||||
}
|
||||
|
||||
/* Буфер пуст — проверяем таймаут. */
|
||||
if (timeout_ms == 0U)
|
||||
{
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ COM-порт (`/dev/ttyACM*` на Linux/macOS, `COMx` на Windows). Испол
|
|||
Скорость: High-Speed (480 Mbit/s), fallback Full-Speed (12 Mbit/s).
|
||||
PHY калибровка: `D_CAL=0x0C`, `TXCAL45DP=0x06`, `TXCAL45DM=0x06`.
|
||||
|
||||
**VID/PID**: `0x1234` / `0x0001` — placeholder, заменить на производственные.
|
||||
**VID/PID**: `0x1996` / `0x00AD` (`usb_device_descriptor.h`) — тот же
|
||||
идентификатор, что `tools/service_tui/` (service-tui) использует для
|
||||
детекта CDC-порта firmware_test (`SERVICE_CDC_VID`/`SERVICE_CDC_PID`).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ extern "C"
|
|||
*
|
||||
* @pre board_hw_init() вызван (MPU настроен, NonCacheable регион активен).
|
||||
*
|
||||
* @return BSP_OK при успехе, BSP_ERR_HW при ошибке инициализациистека.
|
||||
* @return BSP_OK при успехе, BSP_ERR_HW при ошибке инициализации стека.
|
||||
*/
|
||||
bsp_status_t bsp_usb_cdc_init(void);
|
||||
|
||||
|
|
|
|||
|
|
@ -566,8 +566,6 @@ bsp_status_t bsp_usb_cdc_init(void)
|
|||
|
||||
USB_DeviceIsrEnable();
|
||||
|
||||
/* FIXME:Задержка для стабилизации DP pull-down. */
|
||||
//SDK_DelayAtLeastUs(USB_ATTACH_DELAY_US, SDK_DEVICE_MAXIMUM_CPU_CLOCK_FREQUENCY);
|
||||
bsp_delay(USB_ATTACH_DELAY_US / 1000);
|
||||
USB_DeviceRun(g_usbDeviceHandle);
|
||||
|
||||
|
|
|
|||
15
bsp/wdog/CMakeLists.txt
Normal file
15
bsp/wdog/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
if(BUILD_TESTS_HOST)
|
||||
return()
|
||||
endif()
|
||||
|
||||
add_library(bsp_wdog STATIC src/wdog.c)
|
||||
|
||||
target_include_directories(
|
||||
bsp_wdog
|
||||
PUBLIC include/
|
||||
PRIVATE src/)
|
||||
|
||||
target_link_libraries(
|
||||
bsp_wdog
|
||||
PUBLIC bsp_status
|
||||
PRIVATE sdk_wdog)
|
||||
80
bsp/wdog/README.md
Normal file
80
bsp/wdog/README.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# bsp_wdog — аппаратный watchdog (WDOG1)
|
||||
|
||||
Аппаратный сброс МК по таймауту. Защищает от зависаний в блокирующих вызовах,
|
||||
которые не возвращают управление в код приложения — там, где программный
|
||||
watchdog (флаг + проверка в основном цикле) бессилен, поскольку сам цикл не
|
||||
выполняется.
|
||||
|
||||
---
|
||||
|
||||
## Аппаратура
|
||||
|
||||
| Параметр | Значение |
|
||||
| ---------------------- | ---------------------------------------------- |
|
||||
| Периферия | WDOG1 |
|
||||
| Шаг таймаута | 0.5 c |
|
||||
| Диапазон таймаута | 1..128 c |
|
||||
| Причина сброса | `WDOG1->WRSR.TOUT` (1 — сброс был по watchdog) |
|
||||
| Поведение под SWD-halt | Приостановлен (`enableDebug = false`) |
|
||||
|
||||
---
|
||||
|
||||
## Контракт: WDE — write-once
|
||||
|
||||
`WDOG_WCR.WDE` (enable) — бит однократной записи: после `bsp_wdog_init()`
|
||||
watchdog нельзя выключить программно до следующего POR. Он остаётся взведённым
|
||||
и после любого перехода управления внутри той же сессии питания (переход в
|
||||
другой образ прыжком, а не через ресет). Любой код, к которому управление
|
||||
переходит после инициализации watchdog в этой же сессии, обязан периодически
|
||||
вызывать `bsp_wdog_refresh()` не реже периода таймаута — иначе неизбежен
|
||||
reset-loop.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```c
|
||||
bsp_status_t bsp_wdog_init(uint32_t timeout_s); /* взвести, once; захватывает причину предыдущего сброса */
|
||||
void bsp_wdog_refresh(void); /* сбросить счётчик таймаута */
|
||||
bool bsp_wdog_caused_last_reset(void); /* true, если последний сброс МК — по таймауту WDOG */
|
||||
bool bsp_wdog_is_armed(void); /* true после успешного init() */
|
||||
uint32_t bsp_wdog_timeout_s(void); /* сконфигурированный таймаут (0 до init) */
|
||||
```
|
||||
|
||||
`bsp_wdog_refresh()` безопасно звать даже до `bsp_wdog_init()` — no-op.
|
||||
Звать только в точках подтверждённого прогресса, не непосредственно перед
|
||||
вызовом, от зависания в котором watchdog и защищает.
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```c
|
||||
#include "bsp/wdog.h"
|
||||
|
||||
/* main.c — как можно раньше после board_hw_init(): */
|
||||
bsp_wdog_init(10U); /* c запасом над самой длинной легитимной операцией */
|
||||
|
||||
if (bsp_wdog_caused_last_reset())
|
||||
{
|
||||
/* предыдущая сессия закончилась таймаутом — восстановились после зависания */
|
||||
}
|
||||
|
||||
/* в основном цикле и в точках подтверждённого прогресса: */
|
||||
bsp_wdog_refresh();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CMake
|
||||
|
||||
```cmake
|
||||
target_link_libraries(firmware_test PRIVATE bsp_wdog)
|
||||
```
|
||||
|
||||
**Зависимости модуля:**
|
||||
|
||||
| Зависимость | Тип | Описание |
|
||||
| ------------ | ------- | ------------------------------------ |
|
||||
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
|
||||
| `sdk_wdog` | PRIVATE | `fsl_wdog.h` — `WDOG_Init/Refresh()` |
|
||||
60
bsp/wdog/include/bsp/wdog.h
Normal file
60
bsp/wdog/include/bsp/wdog.h
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* bsp_wdog — аппаратный watchdog (WDOG1, MIMXRT1052).
|
||||
*
|
||||
* Назначение: аппаратный сброс МК по таймауту. Защищает от зависаний в
|
||||
* блокирующих вызовах, которые не возвращают управление в код приложения —
|
||||
* там, где программный watchdog (флаг + проверка в основном цикле) бессилен,
|
||||
* поскольку сам цикл не выполняется.
|
||||
*
|
||||
* Контракт: WDOG Enable (WDE) — write-once бит. После bsp_wdog_init() watchdog
|
||||
* нельзя выключить программно до следующего POR; он остаётся взведённым в
|
||||
* течение всей сессии питания, включая любую передачу управления внутри неё
|
||||
* (не только через ресет). Любой код, к которому управление переходит после
|
||||
* инициализации watchdog в этой же сессии, обязан периодически звать
|
||||
* bsp_wdog_refresh() не реже периода таймаута — иначе reset-loop. Под
|
||||
* отладчиком (SWD halt) watchdog приостанавливается (enableDebug=false), так
|
||||
* что пошаговая отладка не сбивается сбросами.
|
||||
*/
|
||||
|
||||
#ifndef BSP_WDOG_H
|
||||
#define BSP_WDOG_H
|
||||
|
||||
#include "bsp/status.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* Взвести WDOG1 с таймаутом timeout_s секунд (аппаратно округляется до шага
|
||||
* 0.5 с: реальный таймаут = (2*timeout_s) * 0.5 c). Разумный диапазон 1..128.
|
||||
*
|
||||
* Побочно захватывает причину ПРЕДЫДУЩЕГО сброса (WDOG-таймаут vs прочее) для
|
||||
* bsp_wdog_caused_last_reset() — читать до/после безразлично, но делается тут.
|
||||
*
|
||||
* Вызывать один раз, как можно раньше в main() (после board_hw_init()).
|
||||
* Повторный вызов — no-op (WDE уже взведён).
|
||||
*/
|
||||
bsp_status_t bsp_wdog_init(uint32_t timeout_s);
|
||||
|
||||
/*
|
||||
* "Погладить" watchdog — сбросить счётчик таймаута. Дёшево; безопасно звать
|
||||
* даже если WDOG не взведён (запись refresh-последовательности безвредна).
|
||||
* Звать только в точках РЕАЛЬНОГО прогресса, НЕ перед блокирующими вызовами,
|
||||
* от зависания в которых watchdog и защищает.
|
||||
*/
|
||||
void bsp_wdog_refresh(void);
|
||||
|
||||
/*
|
||||
* true, если ПОСЛЕДНИЙ сброс МК был вызван таймаутом WDOG (а не power-on /
|
||||
* software / прочим). Валидно после bsp_wdog_init(). Для диагностики: показать
|
||||
* технологу через USB-CDC, что плата восстановилась после зависания.
|
||||
*/
|
||||
bool bsp_wdog_caused_last_reset(void);
|
||||
|
||||
/* true после успешного bsp_wdog_init(). */
|
||||
bool bsp_wdog_is_armed(void);
|
||||
|
||||
/* Сконфигурированный таймаут в секундах (0, если ещё не взведён). */
|
||||
uint32_t bsp_wdog_timeout_s(void);
|
||||
|
||||
#endif /* BSP_WDOG_H */
|
||||
79
bsp/wdog/src/wdog.c
Normal file
79
bsp/wdog/src/wdog.c
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* bsp_wdog — реализация поверх fsl_wdog (WDOG1).
|
||||
*/
|
||||
|
||||
#include "bsp/wdog.h"
|
||||
|
||||
#include "fsl_wdog.h"
|
||||
|
||||
#define BSP_WDOG_BASE WDOG1
|
||||
|
||||
/* Границы поля WCR.WT (8 бит): таймаут = (WT+1) * 0.5 c, максимум 128 c. */
|
||||
#define BSP_WDOG_TIMEOUT_S_MIN 1U
|
||||
#define BSP_WDOG_TIMEOUT_S_MAX 128U
|
||||
|
||||
static bool g_s_armed = false;
|
||||
static bool g_s_last_reset_was_wdog = false;
|
||||
static uint32_t g_s_timeout_s = 0U;
|
||||
|
||||
bsp_status_t bsp_wdog_init(uint32_t timeout_s)
|
||||
{
|
||||
if (g_s_armed)
|
||||
{
|
||||
return BSP_OK; /* WDE — write-once; повторно не взводим */
|
||||
}
|
||||
|
||||
if ((timeout_s < BSP_WDOG_TIMEOUT_S_MIN) || (timeout_s > BSP_WDOG_TIMEOUT_S_MAX))
|
||||
{
|
||||
return BSP_ERR_PARAM;
|
||||
}
|
||||
|
||||
/*
|
||||
* Причина предыдущего сброса: читаем WDOG1->WRSR до настройки. WRSR
|
||||
* read-only, отражает последний сброс (TOUT=WDOG-таймаут, POR=power-on),
|
||||
* стабилен до следующего сброса.
|
||||
*/
|
||||
g_s_last_reset_was_wdog = (BSP_WDOG_BASE->WRSR & WDOG_WRSR_TOUT_MASK) != 0U;
|
||||
|
||||
wdog_config_t cfg;
|
||||
WDOG_GetDefaultConfig(&cfg);
|
||||
|
||||
/* WT = 2*timeout_s - 1 → таймаут = (WT+1)*0.5 c = timeout_s c. */
|
||||
cfg.timeoutValue = (uint16_t) ((timeout_s * 2U) - 1U);
|
||||
|
||||
/*
|
||||
* КРИТИЧНО для рабочего процесса: не сбрасывать плату, когда ядро
|
||||
* остановлено отладчиком (SWD halt) — иначе пошаговая отладка загрузчика
|
||||
* невозможна. enableWait/enableStop оставляем как в дефолте: загрузчик и
|
||||
* приложение в эти режимы не входят, но если войдут — пусть watchdog
|
||||
* продолжает считать (безопаснее по умолчанию).
|
||||
*/
|
||||
cfg.workMode.enableDebug = false;
|
||||
|
||||
cfg.enableWdog = true;
|
||||
WDOG_Init(BSP_WDOG_BASE, &cfg); /* с этого момента WDE взведён навсегда */
|
||||
|
||||
g_s_timeout_s = timeout_s;
|
||||
g_s_armed = true;
|
||||
return BSP_OK;
|
||||
}
|
||||
|
||||
void bsp_wdog_refresh(void)
|
||||
{
|
||||
WDOG_Refresh(BSP_WDOG_BASE);
|
||||
}
|
||||
|
||||
bool bsp_wdog_caused_last_reset(void)
|
||||
{
|
||||
return g_s_last_reset_was_wdog;
|
||||
}
|
||||
|
||||
bool bsp_wdog_is_armed(void)
|
||||
{
|
||||
return g_s_armed;
|
||||
}
|
||||
|
||||
uint32_t bsp_wdog_timeout_s(void)
|
||||
{
|
||||
return g_s_timeout_s;
|
||||
}
|
||||
271
cmake/linker/MIMXRT1052xxxxx_bootloader_flexspi_nor.ld
Normal file
271
cmake/linker/MIMXRT1052xxxxx_bootloader_flexspi_nor.ld
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
** ###################################################################
|
||||
** Processors: MIMXRT1052CVJ5B
|
||||
** MIMXRT1052CVL5B
|
||||
** MIMXRT1052DVJ6B
|
||||
** MIMXRT1052DVL6B
|
||||
**
|
||||
** Compiler: GNU C Compiler
|
||||
** Reference manual: IMXRT1050RM Rev.5, 07/2021 | IMXRT1050SRM Rev.2
|
||||
**
|
||||
** Abstract:
|
||||
** Linker file for firmware/bootloader.
|
||||
**
|
||||
** Вариант MIMXRT1052xxxxx_flexspi_nor.ld с m_text, ограниченным
|
||||
** бюджетом bootloader из docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md —
|
||||
** 256 KB от 0x60000000 (0x60000000..0x60040000). Slot A (tft_app)
|
||||
** начинается на 0x60040000 сразу за границей m_text. Превышение
|
||||
** бюджета — ошибка линковки (ASSERT ниже), а не тихий выход
|
||||
** кода bootloader за пределы своей области.
|
||||
**
|
||||
** Copyright 2016 Freescale Semiconductor, Inc.
|
||||
** Copyright 2016-2024 NXP
|
||||
** SPDX-License-Identifier: BSD-3-Clause
|
||||
** ###################################################################
|
||||
*/
|
||||
|
||||
/* Entry Point */
|
||||
ENTRY(Reset_Handler)
|
||||
|
||||
HEAP_SIZE = DEFINED(__heap_size__) ? __heap_size__ : 0x2000;
|
||||
STACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x1000;
|
||||
VECTOR_RAM_SIZE = DEFINED(__ram_vector_table__) ? 0x00000400 : 0;
|
||||
|
||||
/* Specify the memory areas */
|
||||
MEMORY
|
||||
{
|
||||
m_flash_config (RX) : ORIGIN = 0x60000000, LENGTH = 0x00001000
|
||||
m_ivt (RX) : ORIGIN = 0x60001000, LENGTH = 0x00001000
|
||||
m_interrupts (RX) : ORIGIN = 0x60002000, LENGTH = 0x00000400
|
||||
m_text (RX) : ORIGIN = 0x60002400, LENGTH = 0x0003DC00 /* до 0x60040000 — граница Slot A */
|
||||
m_qacode (RX) : ORIGIN = 0x00000000, LENGTH = 0x00020000 /* SRAM_ITC 128KB */
|
||||
m_data (RW) : ORIGIN = 0x20000000, LENGTH = 0x00020000 /* SRAM_DTC 128KB */
|
||||
m_data2 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 /* SRAM_OC 256KB */
|
||||
}
|
||||
|
||||
/* Define output sections */
|
||||
SECTIONS
|
||||
{
|
||||
__NCACHE_REGION_START = ORIGIN(m_data2);
|
||||
__NCACHE_REGION_SIZE = 0x2000; /* 8 KB non-cacheable for USB DMA */
|
||||
|
||||
.flash_config :
|
||||
{
|
||||
. = ALIGN(4);
|
||||
__FLASH_BASE = .;
|
||||
KEEP(* (.boot_hdr.conf)) /* flash config section */
|
||||
. = ALIGN(4);
|
||||
} > m_flash_config
|
||||
|
||||
ivt_begin = ORIGIN(m_flash_config) + LENGTH(m_flash_config);
|
||||
|
||||
.ivt : AT(ivt_begin)
|
||||
{
|
||||
. = ALIGN(4);
|
||||
KEEP(* (.boot_hdr.ivt)) /* ivt section */
|
||||
KEEP(* (.boot_hdr.boot_data)) /* boot section */
|
||||
KEEP(* (.boot_hdr.dcd_data)) /* dcd section (не используется bootloader — без DCD) */
|
||||
. = ALIGN(4);
|
||||
} > m_ivt
|
||||
|
||||
/* The startup code goes first into internal RAM */
|
||||
.interrupts :
|
||||
{
|
||||
__VECTOR_TABLE = .;
|
||||
__Vectors = .;
|
||||
. = ALIGN(4);
|
||||
KEEP(*(.isr_vector)) /* Startup code */
|
||||
. = ALIGN(4);
|
||||
} > m_interrupts
|
||||
|
||||
/* The program code and other data goes into internal RAM */
|
||||
.text :
|
||||
{
|
||||
. = ALIGN(4);
|
||||
*(.text) /* .text sections (code) */
|
||||
*(.text*) /* .text* sections (code) */
|
||||
*(.rodata) /* .rodata sections (constants, strings, etc.) */
|
||||
*(.rodata*) /* .rodata* sections (constants, strings, etc.) */
|
||||
*(.glue_7) /* glue arm to thumb code */
|
||||
*(.glue_7t) /* glue thumb to arm code */
|
||||
*(.eh_frame)
|
||||
KEEP (*(.init))
|
||||
KEEP (*(.fini))
|
||||
. = ALIGN(4);
|
||||
} > m_text
|
||||
|
||||
.ARM.extab :
|
||||
{
|
||||
*(.ARM.extab* .gnu.linkonce.armextab.*)
|
||||
} > m_text
|
||||
|
||||
.ARM :
|
||||
{
|
||||
__exidx_start = .;
|
||||
*(.ARM.exidx*)
|
||||
__exidx_end = .;
|
||||
} > m_text
|
||||
|
||||
.ctors :
|
||||
{
|
||||
__CTOR_LIST__ = .;
|
||||
/* gcc uses crtbegin.o to find the start of
|
||||
the constructors, so we make sure it is
|
||||
first. Because this is a wildcard, it
|
||||
doesn't matter if the user does not
|
||||
actually link against crtbegin.o; the
|
||||
linker won't look for a file to match a
|
||||
wildcard. The wildcard also means that it
|
||||
doesn't matter which directory crtbegin.o
|
||||
is in. */
|
||||
KEEP (*crtbegin.o(.ctors))
|
||||
KEEP (*crtbegin?.o(.ctors))
|
||||
/* We don't want to include the .ctor section from
|
||||
from the crtend.o file until after the sorted ctors.
|
||||
The .ctor section from the crtend file contains the
|
||||
end of ctors marker and it must be last */
|
||||
KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors))
|
||||
KEEP (*(SORT(.ctors.*)))
|
||||
KEEP (*(.ctors))
|
||||
__CTOR_END__ = .;
|
||||
} > m_text
|
||||
|
||||
.dtors :
|
||||
{
|
||||
__DTOR_LIST__ = .;
|
||||
KEEP (*crtbegin.o(.dtors))
|
||||
KEEP (*crtbegin?.o(.dtors))
|
||||
KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors))
|
||||
KEEP (*(SORT(.dtors.*)))
|
||||
KEEP (*(.dtors))
|
||||
__DTOR_END__ = .;
|
||||
} > m_text
|
||||
|
||||
.preinit_array :
|
||||
{
|
||||
PROVIDE_HIDDEN (__preinit_array_start = .);
|
||||
KEEP (*(.preinit_array*))
|
||||
PROVIDE_HIDDEN (__preinit_array_end = .);
|
||||
} > m_text
|
||||
|
||||
.init_array :
|
||||
{
|
||||
PROVIDE_HIDDEN (__init_array_start = .);
|
||||
KEEP (*(SORT(.init_array.*)))
|
||||
KEEP (*(.init_array*))
|
||||
PROVIDE_HIDDEN (__init_array_end = .);
|
||||
} > m_text
|
||||
|
||||
.fini_array :
|
||||
{
|
||||
PROVIDE_HIDDEN (__fini_array_start = .);
|
||||
KEEP (*(SORT(.fini_array.*)))
|
||||
KEEP (*(.fini_array*))
|
||||
PROVIDE_HIDDEN (__fini_array_end = .);
|
||||
} > m_text
|
||||
|
||||
__etext = .; /* define a global symbol at end of code */
|
||||
__DATA_ROM = .; /* Symbol is used by startup for data initialization */
|
||||
|
||||
.interrupts_ram :
|
||||
{
|
||||
. = ALIGN(4);
|
||||
__VECTOR_RAM__ = .;
|
||||
__interrupts_ram_start__ = .; /* Create a global symbol at data start */
|
||||
*(.m_interrupts_ram) /* This is a user defined section */
|
||||
. += VECTOR_RAM_SIZE;
|
||||
. = ALIGN(4);
|
||||
__interrupts_ram_end__ = .; /* Define a global symbol at data end */
|
||||
} > m_data
|
||||
|
||||
__VECTOR_RAM = DEFINED(__ram_vector_table__) ? __VECTOR_RAM__ : ORIGIN(m_interrupts);
|
||||
__RAM_VECTOR_TABLE_SIZE_BYTES = DEFINED(__ram_vector_table__) ? (__interrupts_ram_end__ - __interrupts_ram_start__) : 0x0;
|
||||
|
||||
.data : AT(__DATA_ROM)
|
||||
{
|
||||
. = ALIGN(4);
|
||||
__DATA_RAM = .;
|
||||
__data_start__ = .; /* create a global symbol at data start */
|
||||
*(.data) /* .data sections */
|
||||
*(.data*) /* .data* sections */
|
||||
*(DataQuickAccess) /* quick access data section */
|
||||
KEEP(*(.jcr*))
|
||||
. = ALIGN(4);
|
||||
__data_end__ = .; /* define a global symbol at data end */
|
||||
} > m_data
|
||||
|
||||
__ram_function_flash_start = __DATA_ROM + (__data_end__ - __data_start__); /* Symbol is used by startup for TCM data initialization */
|
||||
|
||||
.ram_function : AT(__ram_function_flash_start)
|
||||
{
|
||||
. = ALIGN(32);
|
||||
__ram_function_start__ = .;
|
||||
*(CodeQuickAccess)
|
||||
. = ALIGN(128);
|
||||
__ram_function_end__ = .;
|
||||
} > m_qacode
|
||||
|
||||
__NDATA_ROM = __ram_function_flash_start + (__ram_function_end__ - __ram_function_start__);
|
||||
.ncache.init :
|
||||
{
|
||||
. = ALIGN(32);
|
||||
__noncachedata_start__ = .;
|
||||
*(NonCacheable.init)
|
||||
. = ALIGN(4);
|
||||
__noncachedata_init_end__ = .;
|
||||
} > m_data2
|
||||
. = __noncachedata_init_end__;
|
||||
.ncache :
|
||||
{
|
||||
*(NonCacheable)
|
||||
. = ALIGN(4);
|
||||
__noncachedata_end__ = .;
|
||||
} > m_data2
|
||||
|
||||
__DATA_END = __NDATA_ROM;
|
||||
text_end = ORIGIN(m_text) + LENGTH(m_text);
|
||||
ASSERT(__DATA_END <= text_end, "region m_text overflowed with text and data")
|
||||
ASSERT(text_end <= 0x60040000, "bootloader вышел за пределы бюджета 256 KB (граница Slot A, см. BOOTLOADER_FLASH_MAP.md)")
|
||||
ASSERT((__noncachedata_end__ - ORIGIN(m_data2)) <= LENGTH(m_data2), "m_data2 ncache overflow")
|
||||
|
||||
/* Uninitialized data section */
|
||||
.bss :
|
||||
{
|
||||
/* This is used by the startup in order to initialize the .bss section */
|
||||
. = ALIGN(4);
|
||||
__START_BSS = .;
|
||||
__bss_start__ = .;
|
||||
*(.bss)
|
||||
*(.bss*)
|
||||
*(COMMON)
|
||||
. = ALIGN(4);
|
||||
__bss_end__ = .;
|
||||
__END_BSS = .;
|
||||
} > m_data
|
||||
|
||||
.heap :
|
||||
{
|
||||
. = ALIGN(8);
|
||||
__end__ = .;
|
||||
PROVIDE(end = .);
|
||||
__HeapBase = .;
|
||||
. += HEAP_SIZE;
|
||||
__HeapLimit = .;
|
||||
__heap_limit = .; /* Add for _sbrk */
|
||||
} > m_data
|
||||
|
||||
.stack :
|
||||
{
|
||||
. = ALIGN(8);
|
||||
. += STACK_SIZE;
|
||||
} > m_data
|
||||
|
||||
/* Initializes stack on the end of block */
|
||||
__StackTop = ORIGIN(m_data) + LENGTH(m_data);
|
||||
__StackLimit = __StackTop - STACK_SIZE;
|
||||
PROVIDE(__stack = __StackTop);
|
||||
|
||||
.ARM.attributes 0 : { *(.ARM.attributes) }
|
||||
|
||||
ASSERT(__StackLimit >= __HeapLimit, "region m_data overflowed with stack and heap")
|
||||
}
|
||||
130
cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld
Normal file
130
cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
** ###################################################################
|
||||
** Linker file for the GNU C Compiler
|
||||
**
|
||||
** Abstract:
|
||||
** Тестовая заглушка для аппаратной верификации Фазы 2 bootutil
|
||||
** (firmware/bootloader/PLAN.md) — минимальный XIP-образ, который
|
||||
** boot_go() выбирает и в который bootloader реально прыгает.
|
||||
** НЕ boot-образ для BootROM (нет FCB/IVT/DCD секций — Slot A/Б не
|
||||
** видны BootROM напрямую, только software jump из bootloader).
|
||||
**
|
||||
** Базовый адрес слота передаётся через -Wl,--defsym=__slot_base__=0x...
|
||||
** (Slot A: 0x60040000, Slot Б: 0x60240000, см.
|
||||
** docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md). imgtool header (0x200)
|
||||
** зарезервирован перед vector table — boot_select.c вычисляет адрес
|
||||
** прыжка как flash_base + fa_off + ih_hdr_size.
|
||||
** ###################################################################
|
||||
*/
|
||||
|
||||
ENTRY(Reset_Handler)
|
||||
|
||||
HEAP_SIZE = DEFINED(__heap_size__) ? __heap_size__ : 0x400;
|
||||
STACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x400;
|
||||
|
||||
SLOT_BASE = DEFINED(__slot_base__) ? __slot_base__ : 0x60040000;
|
||||
IMGTOOL_HDR_SZ = 0x200; /* -H 0x200 при imgtool sign, см. PLAN.md */
|
||||
|
||||
MEMORY
|
||||
{
|
||||
m_interrupts (RX) : ORIGIN = SLOT_BASE + IMGTOOL_HDR_SZ, LENGTH = 0x00000400
|
||||
m_text (RX) : ORIGIN = SLOT_BASE + IMGTOOL_HDR_SZ + 0x400, LENGTH = 0x00008000 /* 32 KB — с большим запасом для мигалки */
|
||||
m_data (RW) : ORIGIN = 0x20000000, LENGTH = 0x00020000 /* SRAM_DTC 128KB */
|
||||
m_data2 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 /* SRAM_OC 256KB — не используется стабом, нужна board_mpu_init() */
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
{
|
||||
__NCACHE_REGION_START = ORIGIN(m_data2);
|
||||
__NCACHE_REGION_SIZE = 0x2000; /* 8 KB non-cacheable, как в остальных линкер-скриптах */
|
||||
|
||||
.interrupts :
|
||||
{
|
||||
__VECTOR_TABLE = .;
|
||||
__Vectors = .;
|
||||
. = ALIGN(4);
|
||||
KEEP(*(.isr_vector))
|
||||
. = ALIGN(4);
|
||||
} > m_interrupts
|
||||
|
||||
.text :
|
||||
{
|
||||
. = ALIGN(4);
|
||||
*(.text)
|
||||
*(.text*)
|
||||
*(.rodata)
|
||||
*(.rodata*)
|
||||
*(.glue_7)
|
||||
*(.glue_7t)
|
||||
*(.eh_frame)
|
||||
KEEP (*(.init))
|
||||
KEEP (*(.fini))
|
||||
. = ALIGN(4);
|
||||
} > m_text
|
||||
|
||||
.ARM.extab : { *(.ARM.extab* .gnu.linkonce.armextab.*) } > m_text
|
||||
|
||||
.ARM :
|
||||
{
|
||||
__exidx_start = .;
|
||||
*(.ARM.exidx*)
|
||||
__exidx_end = .;
|
||||
} > m_text
|
||||
|
||||
.init_array :
|
||||
{
|
||||
PROVIDE_HIDDEN (__init_array_start = .);
|
||||
KEEP (*(SORT(.init_array.*)))
|
||||
KEEP (*(.init_array*))
|
||||
PROVIDE_HIDDEN (__init_array_end = .);
|
||||
} > m_text
|
||||
|
||||
__etext = .;
|
||||
__DATA_ROM = .;
|
||||
|
||||
.data : AT(__DATA_ROM)
|
||||
{
|
||||
. = ALIGN(4);
|
||||
__data_start__ = .;
|
||||
*(.data)
|
||||
*(.data*)
|
||||
. = ALIGN(4);
|
||||
__data_end__ = .;
|
||||
} > m_data
|
||||
|
||||
.bss :
|
||||
{
|
||||
. = ALIGN(4);
|
||||
__bss_start__ = .;
|
||||
*(.bss)
|
||||
*(.bss*)
|
||||
*(COMMON)
|
||||
. = ALIGN(4);
|
||||
__bss_end__ = .;
|
||||
} > m_data
|
||||
|
||||
.heap :
|
||||
{
|
||||
. = ALIGN(8);
|
||||
__end__ = .;
|
||||
PROVIDE(end = .);
|
||||
__HeapBase = .;
|
||||
. += HEAP_SIZE;
|
||||
__HeapLimit = .;
|
||||
__heap_limit = .;
|
||||
} > m_data
|
||||
|
||||
.stack :
|
||||
{
|
||||
. = ALIGN(8);
|
||||
. += STACK_SIZE;
|
||||
} > m_data
|
||||
|
||||
__StackTop = ORIGIN(m_data) + LENGTH(m_data);
|
||||
__StackLimit = __StackTop - STACK_SIZE;
|
||||
PROVIDE(__stack = __StackTop);
|
||||
|
||||
.ARM.attributes 0 : { *(.ARM.attributes) }
|
||||
|
||||
ASSERT(__StackLimit >= __HeapLimit, "region m_data overflowed with stack and heap")
|
||||
}
|
||||
|
|
@ -119,7 +119,7 @@ set(CMAKE_ASM_FLAGS_RELEASE
|
|||
CACHE INTERNAL "")
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# C runtime библиотека — newlib-nano (меньше размер, подходит для embedded)
|
||||
# C runtime библиотека — newlib-nano
|
||||
# -----------------------------------------------------------------------------
|
||||
# --specs=nano.specs — использовать newlib-nano: облегчённая stdlib,
|
||||
# меньший размер printf/malloc -Wl,--no-warn-rwx-segments — подавить
|
||||
|
|
|
|||
198
docs/CI_WORKFLOW.md
Normal file
198
docs/CI_WORKFLOW.md
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
# 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 пока не встроена (сама
|
||||
церемония — не автоматизируемый процесс, см. документ).
|
||||
|
|
@ -198,9 +198,9 @@ flowchart LR
|
|||
│ │ │ dcd.bin, ivt_flashloader.bin
|
||||
│ │ └── uv.lock
|
||||
│ │
|
||||
│ ├── production/ ← service-tui: TUI сервисного инженера (Textual)
|
||||
│ ├── service_tui/ ← service-tui: TUI сервисного инженера (Textual)
|
||||
│ │ прошивка/диагностика готовых плат, см.
|
||||
│ │ tools/production/README.md + DEV_ARCH.md
|
||||
│ │ tools/service_tui/README.md
|
||||
│ │
|
||||
│ └── hil/ ← HIL pytest-окружение
|
||||
│ ├── conftest.py ← фикстуры: m5, loaded_<n>, uart_<n>
|
||||
|
|
@ -210,6 +210,7 @@ flowchart LR
|
|||
│ ├── 01_test_uart.py ← HIL тест bsp_uart_host (без M5)
|
||||
│ ├── 02_test_opto.py ← HIL тест bsp_opto (через M5StampPLC)
|
||||
│ ├── 03_test_can.py ← HIL тест bsp_can
|
||||
│ ├── 04_test_button.py ← HIL тест bsp_button (интерактивный, оператор)
|
||||
│ ├── 05_test_usb_cdc.py ← HIL тест USB CDC (bsp_usb_cdc, UART CLI)
|
||||
│ ├── 06_test_firmware_opto.py ← HIL тест opto через firmware_test CDC
|
||||
│ ├── 06_test_firmware_can.py ← HIL тест CAN через firmware_test CDC
|
||||
|
|
@ -355,8 +356,8 @@ buildPresets (HIL):
|
|||
| Прошивка | Стратегия | Инструмент загрузки |
|
||||
| ---------------------------- | ---------------------------------- | ------------------- |
|
||||
| `firmware_test` | XIP из Flash (`flexspi_nor.ld`) | SPSDK → Flash |
|
||||
| `bootloader` | Копирование в ITCM | SPSDK → Flash |
|
||||
| `tft_app` | XIP + буферы в SDRAM | SPSDK → Flash |
|
||||
| `bootloader` | XIP из Flash, без ITCM/DCD (не трогает SDRAM) — выбирает и запускает `tft_app` из слота (MCUboot Direct-XIP) | SPSDK → Flash |
|
||||
| `tft_app` | XIP из своего слота (Direct-XIP, два слота A/Б) + буферы в SDRAM (SEMC поднимает сама) | SPSDK → Flash |
|
||||
| HIL target (`tests/target/`) | Исполнение из ITCM/DTCM (`ram.ld`) | pyOCD → RAM |
|
||||
|
||||
**HIL boot-стратегия:** pyOCD настраивает FLEXRAM (128 KB ITCM + 128 KB DTCM + 256 KB OCRAM), записывает PT_LOAD сегменты ELF по физическим адресам, устанавливает SP/PC из таблицы векторов и запускает выполнение. Flash не используется — прошивка исчезает при отключении питания.
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ just host::flash-production # bootloader release + app release (с подт
|
|||
flowchart TD
|
||||
A["Плата в SDP-режиме\n1FC9:0130"] --> B["sdphost\nзагрузить ivt_flashloader.bin\nв RAM 0x20001C00"]
|
||||
B --> C["sdphost jump-address\nFlashloader поднимается\nкак 15A2:0073"]
|
||||
C --> D["configure-memory 0xC0000007\nинициализация FlexSPI NOR"]
|
||||
C --> D["configure-memory 0xC0000207\nинициализация FlexSPI NOR + QE-бит"]
|
||||
D --> E["flash-erase-region 0x60000000"]
|
||||
E --> F["configure-memory 0xF000000F\nзапись FCB в 0x60000000"]
|
||||
F --> G["write-memory 0x60001000\nHAB-образ"]
|
||||
|
|
@ -79,18 +79,23 @@ auto-config не подтверждена — см. 1.5.
|
|||
|
||||
### 1.5 Нестандартная память (W25Q256/512) и сторонние бинарники
|
||||
|
||||
`service-tui` (`tools/production/`) умеет прошивать бинарники, собранные не
|
||||
`service-tui` (`tools/service_tui/`) умеет прошивать бинарники, собранные не
|
||||
в этом репозитории (например, старые платы с W25Q512), тем же способом
|
||||
(USB SDP), но с двумя отличиями от штатного пути:
|
||||
(USB SDP), но с двумя отличиями от штатного пути. Это **отдельная
|
||||
реализация**, не связанная с `flash_usb.py`/`nxpimage` CLI — TUI прошивает
|
||||
in-process через Python API `spsdk` (`app/flash_backend.py`: `HabImage`,
|
||||
`McuBoot`, `SDP`), без единого subprocess:
|
||||
|
||||
- HAB-образ (IVT + опционально DCD) собирается из **сырого** бинарника на
|
||||
лету через `nxpimage`, а не заранее через `just build::hab-*`
|
||||
- FCB пишется **явно** (`flash_usb.py --fcb-path tools/host/dcd/w25qXXX_fdcb.bin`,
|
||||
буквальный `write-memory` вместо `configure-memory 0xF000000F`) — auto-config
|
||||
для 4-байтной адресации не проверялся, решили на него не полагаться
|
||||
лету через `HabImage` (spsdk), а не заранее через `just build::hab-*`
|
||||
- FCB пишется **явно** (`mboot.write_memory()` с готовым блобом
|
||||
`tools/host/dcd/w25qXXX_fdcb.bin`, буквальная запись вместо
|
||||
`configure-memory 0xF000000F`) — auto-config для 4-байтной адресации не
|
||||
проверялся, решили на него не полагаться
|
||||
|
||||
Подробности конвейера — в `tools/production/DEV_ARCH.md`, §8. Штатный путь
|
||||
(`--firmware`, три сборки этого репозитория) не меняется и по-прежнему
|
||||
Подробности конвейера — в [tools/service_tui/docs/ARCHITECTURE.md](../tools/service_tui/docs/ARCHITECTURE.md),
|
||||
§8. Штатный путь (`--firmware`, три сборки этого репозитория, что через
|
||||
`just host::flash`, что через `service-tui`) не меняется и по-прежнему
|
||||
использует auto-config Flashloader, как описано в 1.4.
|
||||
|
||||
---
|
||||
|
|
|
|||
125
docs/bootloader/BOOTLOADER_FLASH_MAP.md
Normal file
125
docs/bootloader/BOOTLOADER_FLASH_MAP.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Карта Flash — bootloader + tft_app (A/Б) + файловая система ассетов
|
||||
|
||||
Единая карта QSPI NOR Flash (W25Q64/128/256/512, см. [bsp/qspi_flash](../../bsp/qspi_flash/README.md))
|
||||
для связки `firmware/bootloader` + `firmware/tft_app`. Дополняет
|
||||
[BOOT_FLAGS.md](BOOT_FLAGS.md) (сценарии исполнения) и [HAB_GUIDE.md](HAB_GUIDE.md) (подпись).
|
||||
|
||||
---
|
||||
|
||||
## 1. Принцип: один bootloader на любую ёмкость чипа
|
||||
|
||||
`bsp_qspi_flash` определяет чип и его размер в рантайме (`bsp_qspi_init()` → JEDEC ID →
|
||||
`bsp_qspi_flash_size()`), а не на этапе компиляции. На производстве возможен разброс чипов —
|
||||
минимум W25Q128 (16 МБ), также встречается W25Q512 (64 МБ).
|
||||
|
||||
Карта построена так, чтобы **один и тот же бинарник bootloader** работал на любой ёмкости:
|
||||
|
||||
- **Bootloader, Slot A, Slot Б — фиксированные смещения и размеры**, одинаковые на всех платах
|
||||
независимо от реальной ёмкости чипа. Именно это делает образ bootloader переносимым между платами
|
||||
с разным флешем без пересборки.
|
||||
- **Область файловой системы ассетов (спрайты/музыка) — не фиксирована.** Она занимает всё
|
||||
пространство от конца Slot Б до фактического конца чипа, размер вычисляется в рантайме через
|
||||
`bsp_qspi_flash_size()` при монтировании. На 16 МБ чипе это ~11.75 МБ, на 64 МБ — ~59.75 МБ.
|
||||
**Формат и владелец этой области (что её монтирует, кто и как обновляет ассеты) — открытый вопрос,
|
||||
вне рамок текущего плана bootloader'а.** Здесь фиксируется только адресный диапазон.
|
||||
|
||||
`bootutil` (MCUboot, Direct-XIP) знает только про Slot A и Slot Б через `sysflash.h` — про область
|
||||
ФС ему знать не нужно, коллизий с его логикой нет.
|
||||
|
||||
---
|
||||
|
||||
## 2. Карта
|
||||
|
||||
| Область | Смещение от `0x60000000` | Размер | Абсолютный адрес (начало) |
|
||||
| ---------------- | ------------------------- | -------------------------------------- | --------------------------- |
|
||||
| Bootloader | `0x000000` | 256 КБ (`0x040000`) | `0x60000000` |
|
||||
| Slot A (tft_app) | `0x040000` | 2 МБ (`0x200000`) | `0x60040000` |
|
||||
| Slot Б (tft_app) | `0x240000` | 2 МБ (`0x200000`) | `0x60240000` |
|
||||
| ФС ассетов | `0x440000` | `bsp_qspi_flash_size() - 0x440000` | `0x60440000` |
|
||||
|
||||
Все границы выровнены на 64 КБ блок (`BSP_QSPI_BLOCK_64K_SIZE`) — стирание region-ов через
|
||||
`bsp_qspi_erase_block_64k()` без частичных секторов.
|
||||
|
||||
Direct-XIP не использует scratch-область — метаданные состояния/версии образа хранятся в trailer
|
||||
самого образа в каждом слоте (стандартный механизм bootutil), отдельного региона под них не нужно.
|
||||
|
||||
---
|
||||
|
||||
## 3. Обоснование размеров
|
||||
|
||||
**Bootloader — 256 КБ.** Без SDRAM/дисплея/FreeRTOS: bring-up, USB CDC, FatFS, bootutil (Direct-XIP,
|
||||
без swap/scratch-машинерии), крипто-бэкенд (mbedTLS/tinycrypt), драйвер QSPI. Реальный размер ожидается
|
||||
существенно меньше — заложен запас на будущий рост (например RSA вместо ECDSA, расширенная диагностика).
|
||||
|
||||
**Slot A/Б — 2 МБ каждый.** Ассеты (спрайты, музыка) вынесены в отдельную область ФС и не входят в
|
||||
подписанный образ — в слоте только код: FreeRTOS, логика индикатора, шрифты как вшитые C-массивы (8bpp
|
||||
со сглаживанием). Ориентир: текущая production-прошивка (Debug-сборка, с шрифтами, FreeRTOS, FatFS)
|
||||
занимает ~1 МБ — 2 МБ даёт двукратный запас.
|
||||
|
||||
**Почему размеры не пересматриваются "по факту" после первой сборки.** Карта Flash — контракт,
|
||||
зашитый в уже прошитые на производстве bootloader'ы (обновляются только через SWD/USB ROM, не в поле).
|
||||
Смещения слотов фиксированы заранее с запасом, а не подгоняются под фактический размер первой сборки
|
||||
tft_app.
|
||||
|
||||
---
|
||||
|
||||
## 4. Открытые вопросы (не в рамках плана bootloader)
|
||||
|
||||
- Формат и владелец области ФС ассетов (спрайты/музыка): FAT/LittleFS/кастомный, подписывается ли,
|
||||
как обновляется (та же SD-логика, что и Slot A/Б, или отдельный механизм).
|
||||
- Точный layout `.ld`-скрипта tft_app для двух адресов слотов (Direct-XIP: код обычно не
|
||||
позиционно-независим — вероятно потребуется два варианта линковки под Slot A и Slot Б, либо PIC).
|
||||
**Разобрано** в [UPDATE_FLOW.md](UPDATE_FLOW.md): рекомендован dual-link (два подписанных бинаря на
|
||||
версию, как `test_stub`), с выбором линковки по целевому слоту при SD-обновлении; PIC — не сейчас.
|
||||
|
||||
---
|
||||
|
||||
## 5. Ограничения на runtime-доступ к flash из tft_app (Direct-XIP) — решено
|
||||
|
||||
Boot-стратегия (Direct-XIP и для bootloader, и для tft_app — см. `firmware/bootloader/PLAN.md`)
|
||||
пересмотрена и подтверждена в обсуждении Фазы 3, с учётом требований tft_app: (1) кеширование
|
||||
спрайтов в SDRAM, (2) хранение и изменение настроек во flash, (3) проигрывание WAV с flash.
|
||||
|
||||
**Найденный механизм риска** — [bsp/qspi_flash/README.md](../../bsp/qspi_flash/README.md), раздел
|
||||
"XIP-безопасность": любая IP-команда FlexSPI блокирует AHB-путь — если в этот момент CPU фетчит
|
||||
инструкцию из Flash, происходит HardFault. Защита в `bsp_qspi_flash` — IRQ lock на всё время операции
|
||||
(стирание сектора ~45 мс, блока 64 КБ ~150 мс). Для однопоточного блокирующего bootloader'а это не
|
||||
проблема; для tft_app (FreeRTOS, конкурентные задачи) любая такая операция глушит **все** прерывания
|
||||
в системе на своё время, включая аудио DMA-колбэк.
|
||||
|
||||
**Почему не перешли на `MCUBOOT_RAM_LOAD`** (альтернатива, устраняющая конфликт полностью — код
|
||||
перестаёт исполняться через flash-AHB вообще): в вендоренном bootutil этот режим **не имеет аналога
|
||||
`MCUBOOT_DIRECT_XIP_REVERT`** — `boot_select_or_erase()` (`copy_done`/`image_ok`, автоматический откат
|
||||
неподтверждённого образа) гейтится `#if defined(MCUBOOT_DIRECT_XIP) && defined(MCUBOOT_DIRECT_XIP_REVERT)`
|
||||
в `loader.c` и не вызывается в ветке `MCUBOOT_RAM_LOAD`. Переход потерял бы anti-brick гарантию,
|
||||
аппаратно проверенную в Фазе 2 (сценарий 5 её чек-листа), без готовой замены в самом bootutil —
|
||||
пришлось бы реализовывать такой механизм самостоятельно, без прецедента.
|
||||
|
||||
**Референс для калибровки** — легаси-реализация (`TFT8_RX_wOS`, исполняется из SDRAM):
|
||||
её `audio_player.c` стримит блоками `MONO_READ_SIZE=256` Б через кольцевой буфer `BUFFER_NUM=3` — на
|
||||
руках держится всего ~8.7 мс аудио (128 сэмплов / 44100 Гц × 3 блока). Этого достаточно при исполнении
|
||||
из RAM (конкуренции за flash-AHB нет вообще), но недостаточно при Direct-XIP.
|
||||
|
||||
**Решение — Direct-XIP остаётся, при двух обязательных ограничениях для tft_app** (переносятся в его
|
||||
будущий план, не в план bootloader'а):
|
||||
|
||||
1. **Аудио — глубоко буферизировать в SDRAM**, не стримить малыми порциями, как в легаси-версии.
|
||||
Целевая глубина — заведомо больше худшей flash-операции (например, ≥300 мс — это ~26 КБ моно PCM16
|
||||
44.1 кГц, ничто относительно 32 МБ SDRAM). При такой глубине редкая конкурентная запись настроек не
|
||||
создаёт слышимого дропаута.
|
||||
2. **Любое чтение/запись flash, способное совпасть по времени с другой flash-операцией, обязано идти
|
||||
через защищённый IP-command драйвер** (аналог `bsp_qspi_read()`: ITCM + IRQ lock, как уже сделано в
|
||||
`bsp_qspi_flash`), а не через сырой XIP `memcpy`, как в легаси `settings_manager.c` (там это было
|
||||
безопасно только потому что код исполнялся из RAM). Актуально для будущей FatFS-прослойки над
|
||||
областью ассетов (см. п.4 выше) — она должна использовать тот же паттерн, что `port/fatfs/sd`
|
||||
использует поверх `bsp_sd`.
|
||||
3. Запись настроек остаётся редкой, явной, инициированной пользователем (не периодический автосейв) —
|
||||
короткий блокирующий фриз (десятки мс) на сохранение ожидаем и допустим в UI.
|
||||
|
||||
---
|
||||
|
||||
## 6. Ссылки
|
||||
|
||||
- [BOOT_FLAGS.md](BOOT_FLAGS.md) — XIP/DCD/сценарии исполнения кода.
|
||||
- [HAB_GUIDE.md](HAB_GUIDE.md) — подпись bootloader (HAB) vs подпись образов tft_app (`imgtool`).
|
||||
- [bsp/qspi_flash/README.md](../../bsp/qspi_flash/README.md) — поддерживаемые чипы, `bsp_qspi_flash_size()`.
|
||||
302
docs/bootloader/BOOT_FLOW.md
Normal file
302
docs/bootloader/BOOT_FLOW.md
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
# Загрузчик TFT — принцип работы
|
||||
|
||||
Документ описывает поведение загрузчика: как устроена память, как выбирается и обновляется образ
|
||||
приложения, правила версий, даунгрейд и режим восстановления.
|
||||
|
||||
---
|
||||
|
||||
## 1. Карта памяти
|
||||
|
||||
Приложение хранится в QSPI NOR Flash в **двух слотах** — A и Б. Каждый слот содержит **полную,
|
||||
самостоятельно валидную** копию приложения. Загрузчик занимает начало flash, за слотами идёт
|
||||
отдельная область под ассеты (спрайты, звук), которая к процессу загрузки отношения не имеет.
|
||||
|
||||
| Область | Начало | Размер |
|
||||
| ------------------------ | ------------ | ------------- |
|
||||
| Загрузчик | `0x60000000` | 256 КБ |
|
||||
| Slot A | `0x60040000` | 2 МБ |
|
||||
| Slot Б | `0x60240000` | 2 МБ |
|
||||
| Файловая система ассетов | `0x60440000` | до конца чипа |
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
BL["Загрузчик — 256 КБ<br/>0x60000000"]
|
||||
SA["Slot A — 2 МБ<br/>0x60040000"]
|
||||
SB["Slot Б — 2 МБ<br/>0x60240000"]
|
||||
FS["Файловая система ассетов<br/>0x60440000 … конец чипа"]
|
||||
BL --- SA --- SB --- FS
|
||||
```
|
||||
|
||||
**Приложение исполняется прямо из flash** (XIP) — из того слота, который выбрал
|
||||
загрузчик, без копирования в ОЗУ. Из этого следует ключевое свойство: образ жёстко привязан к адресу
|
||||
своего слота при сборке. Поэтому **релиз приложения — это два бинарника на одну версию**: один собран
|
||||
под адрес Slot A, второй — под адрес Slot Б. Образ, физически положенный не в «свой» слот, пройдёт
|
||||
проверку подписи, но не запустится.
|
||||
|
||||
Два слота нужны для безопасного обновления: пока приложение работает из одного слота, новый образ
|
||||
пишется в другой. Рабочая копия никогда не затирается — на диске всегда есть чем загрузиться, даже
|
||||
если обновление прервётся на середине.
|
||||
|
||||
---
|
||||
|
||||
## 2. Выбор образа при старте
|
||||
|
||||
На каждой подаче питания загрузчик решает, из какого слота запускать приложение.
|
||||
|
||||
**Слот считается кандидатом, только если он валиден целиком**: корректная сигнатура формата, целый
|
||||
хэш содержимого и верная криптографическая подпись (ECDSA-P256). Битый или неподписанный слот
|
||||
игнорируется.
|
||||
|
||||
Правила выбора:
|
||||
- Оба слота валидны → активным становится слот с **большей версией**.
|
||||
- Валиден только один → он и активен.
|
||||
- Ни одного валидного → активного слота нет, загрузчик переходит в ожидание microSD.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START([Подача питания]) --> CHECK["Проверить оба слота:<br/>сигнатура + хэш + подпись"]
|
||||
CHECK --> CMP{Сколько валидных?}
|
||||
CMP -->|Оба| HIGHER["Активный = слот<br/>с большей версией"]
|
||||
CMP -->|Один| ONE["Активный = он"]
|
||||
CMP -->|Ни одного| NONE["Ожидание microSD"]
|
||||
HIGHER --> JUMP([Запуск приложения])
|
||||
ONE --> JUMP
|
||||
```
|
||||
|
||||
**Защитная сеть от битого обновления.** Свежеустановленный образ считается «непроверенным», пока сам
|
||||
не подтвердит своё здоровье в рантайме. Если непроверенный образ запустился, но так и не подтвердился
|
||||
(например, завис на старте) — при следующей загрузке он трактуется как неудавшееся обновление и
|
||||
**автоматически стирается**, а загрузчик откатывается на прежний слот. Приложение обязано подтвердить
|
||||
себя один раз, доказав работоспособность.
|
||||
|
||||
---
|
||||
|
||||
## 3. Обновление через microSD
|
||||
|
||||
**Единственный полевой канал обновления — карта microSD.** Загрузчик ищет в корне карты файл
|
||||
`TFT_APP.BIN` — подписанный образ приложения. Карта проверяется при старте и периодически (примерно
|
||||
раз в 1.5 с), пока загрузчик находится в ожидании, — карту можно вставить уже после включения.
|
||||
|
||||
Приёмка кандидата — **двухступенчатая**:
|
||||
1. **Проверка заголовка** (сигнатура формата + версия) прямо с карты, до касания flash — этого
|
||||
достаточно, чтобы решить «ставить или пропустить».
|
||||
2. **Полная криптографическая проверка** — уже после записи в целевой слот. Если подпись битая,
|
||||
только что записанный слот просто не будет выбран при загрузке, и загрузчик останется на прежнем
|
||||
валидном образе.
|
||||
|
||||
Запись идёт **потоком по частям**, каждая записанная часть немедленно вычитывается обратно и
|
||||
сверяется — битая страница ловится сразу.
|
||||
|
||||
**Целевой слот установки — всегда НЕ активный.** Работающий/загружаемый слот не перезаписывается
|
||||
никогда. Если активного слота нет вообще (чистая плата) — по умолчанию Slot A.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CARD([microSD + TFT_APP.BIN]) --> HDR["Пик заголовка:<br/>сигнатура + версия"]
|
||||
HDR -->|Сигнатура битая| REJ1["Отклонить<br/>(candidate invalid)"]
|
||||
HDR -->|OK| DEC{Решение по версии}
|
||||
DEC -->|Ставить| WRITE["Записать в НЕактивный слот:<br/>стереть → поток + verify"]
|
||||
DEC -->|Пропустить| SKIP["Пропустить<br/>(update skipped)"]
|
||||
WRITE --> CRYPTO{"Крипто-проверка<br/>записанного слота"}
|
||||
CRYPTO -->|OK| DONE([Установлено → загрузка])
|
||||
CRYPTO -->|Подпись битая| REJ2["Отклонить<br/>(install rejected)"]
|
||||
```
|
||||
|
||||
**Заводской сценарий** — частный случай этой же логики: чистая плата с одним загрузчиком, оба слота
|
||||
пусты. Загрузчик ждёт SD, при появлении `TFT_APP.BIN` ставит его в Slot A и загружается. Никакого
|
||||
отдельного механизма первичной заливки нет.
|
||||
|
||||
---
|
||||
|
||||
## 4. Правила версий
|
||||
|
||||
Версия образа — **major.minor.revision**. Номер сборки (build) в сравнении **не участвует**: два
|
||||
образа, отличающиеся только номером сборки, считаются равными.
|
||||
|
||||
Версия используется дважды:
|
||||
- при **выборе** активного слота — побеждает бо́льшая версия;
|
||||
- при **решении об установке** кандидата с SD.
|
||||
|
||||
Решение по кандидату (без удержания кнопки):
|
||||
|
||||
| Кандидат относительно активного | Действие |
|
||||
| ------------------------------- | ---------------------------- |
|
||||
| Строго новее | Установить в неактивный слот |
|
||||
| Активного слота нет вообще | Установить в Slot A |
|
||||
| Старше или равен | Пропустить |
|
||||
|
||||
При штатном обновлении (кандидат новее) прежний активный слот **не стирается** — он естественным
|
||||
образом проиграет сравнение версий при следующей загрузке, новый образ победит сам.
|
||||
|
||||
---
|
||||
|
||||
## 5. Даунгрейд
|
||||
|
||||
Установить образ **старее** уже стоящего можно только с помощью оператора: **удержать `BTN 1` в
|
||||
момент подачи питания**. Кнопка считывается один раз на старте и действует всю сессию.
|
||||
|
||||
Ключевой момент: при обычном даунгрейде записать старый образ в свободный слот **недостаточно** —
|
||||
прежний (более новый) активный слот остался бы валиден и снова победил бы по версии, и даунгрейд
|
||||
физически лёг бы на flash, но не загрузился. Поэтому при форсированном даунгрейде прежний активный
|
||||
слот **стирается** — но только **после** того, как новый образ уже записан и подтверждён валидным.
|
||||
На диске никогда не бывает нуля рабочих слотов даже на середине операции.
|
||||
|
||||
| Условие | Действие |
|
||||
| ---------------------------------- | ----------------------------------------------------- |
|
||||
| Кандидат старше + `BTN 1` удержана | Установить в свободный слот, стереть прежний активный |
|
||||
| Кандидат равен активному + `BTN 1` | Пропустить (переустановку той же версии не форсируем) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Режим восстановления (Recovery Mode)
|
||||
|
||||
Назначение — не дать полевой плате превратиться в «кирпич», если уже установленный образ зависает в
|
||||
рантайме, и дать оператору ручной аварийный вход.
|
||||
|
||||
### Аппаратный сторож
|
||||
|
||||
Плата защищена аппаратным watchdog с таймаутом **10 секунд**. Если управление зависает где-либо
|
||||
(включая рантайм приложения), через 10 с происходит аппаратный сброс. Watchdog взводится один раз и
|
||||
до перезагрузки по питанию не выключается — он «переживает» переход в приложение, поэтому приложение
|
||||
обязано периодически его «кормить». Зависание → гарантированный сброс, а не вечный локап.
|
||||
|
||||
### Счётчик и порог
|
||||
|
||||
Число **подряд идущих** watchdog-сбросов хранится в регистре, который переживает тёплый/watchdog-сброс
|
||||
и обнуляется только при настоящей подаче питания (POR). Счётчик обнуляется также при успешной
|
||||
установке нового образа и при откате на фолбэк. Порог срабатывания — **3** сброса подряд.
|
||||
|
||||
### Классификация отказов и их обработка
|
||||
|
||||
| Класс | Ситуация | Что срабатывает |
|
||||
| ----- | ----------------------------------------------- | ----------------------------------------------------- |
|
||||
| **A** | Новый образ завис, ещё не подтвердив себя | Watchdog-сброс + автоматический откат на прежний слот |
|
||||
| **B** | Уже подтверждённый образ завис в рантайме | Счётчик сбросов достиг порога → фолбэк на второй слот |
|
||||
| **C** | Откатываться некуда (единственный/оба зависают) | Recovery Mode |
|
||||
| **D** | Оператор хочет чистый старт вручную | Recovery Mode по `BTN 2` |
|
||||
|
||||
Класс A — самый частый — закрыт полностью автоматически: откат непроверенного образа не требует ни
|
||||
счётчика, ни вмешательства. Класс B ловит то, что откат не покрывает (образ-то подтверждён): после
|
||||
порога зависший слот стирается, и загружается второй, если он валиден.
|
||||
|
||||
### Решение при старте
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S([Начало попытки]) --> BTN{BTN 2 удержана?}
|
||||
BTN -->|Да| REC[Recovery Mode]
|
||||
BTN -->|Нет| CNT{"Счётчик сбросов<br/>≥ порога (3)?"}
|
||||
CNT -->|Нет| NORM[Обычная загрузка]
|
||||
CNT -->|Да| FB{Второй слот валиден?}
|
||||
FB -->|Да| ERASE["Стереть зависший слот →<br/>обнулить счётчик →<br/>загрузить второй"]
|
||||
FB -->|Нет| REC
|
||||
```
|
||||
|
||||
`BTN 2` проверяется **первым** — приоритет ручного входа выше и счётчика, и обычной загрузки, и
|
||||
кнопки даунгрейда. Если `BTN 2` удержана, обычный путь загрузки не выполняется вообще, даже при
|
||||
наличии валидного образа.
|
||||
|
||||
### Поведение в Recovery Mode
|
||||
|
||||
- **Прыжок в приложение подавлен** — это само по себе разрывает цикл зависаний.
|
||||
- **Отдельная LED-индикация**: оба светодиода мигают синхронно, 100 мс включено / 100 мс выключено —
|
||||
явно отличается от heartbeat и рабочих паттернов приложения.
|
||||
- **Статус по USB**: `recovery_mode`.
|
||||
- **Ослабленный контроль версий**: принимается **любой** подписанный образ с SD — без сравнения
|
||||
версий и без кнопки. Проверка подписи при этом сохраняется всегда.
|
||||
- При найденном валидном образе — **оба слота стираются**, образ ставится в Slot A, происходит
|
||||
автоматический прыжок. «Чистый борт» достигается ровно тогда, когда есть чем заменить.
|
||||
|
||||
### Семантика «на одну сессию»
|
||||
|
||||
Счётчик не сохраняется во flash. На подаче питания он обнуляется, поэтому зависший слот **пробуется
|
||||
заново** — если зависание было случайным (транзиентным), плата получает новый шанс. Если зависание
|
||||
детерминированное, оператор жмёт `BTN 2` и входит в recovery немедленно, не дожидаясь порога.
|
||||
|
||||
### Честная граница
|
||||
|
||||
Если образ стабильно работает, обнуляет счётчик (доказав здоровье), и лишь **потом** ловит редкий баг
|
||||
(конкретный файл на SD, конкретное входное сообщение) — счётчик каждый раз обнуляется до зависания,
|
||||
автопорог не накапливается, и цикл автоматически не ловится. Это принципиально: по таймеру не отличить
|
||||
«здоров» от «здоров, но потом словил редкое». В таком случае плата видимо циклится (watchdog +
|
||||
recovery-индикация это показывают), лечится SD-фиксом или `BTN 2`. Watchdog как минимум не даёт плате
|
||||
зависнуть намертво.
|
||||
|
||||
---
|
||||
|
||||
## 7. Индикация и обратная связь
|
||||
|
||||
### Светодиоды
|
||||
|
||||
Полный словарь (установка, «железо не в порядке», ошибка образа, приоритет между ними) —
|
||||
[LED_PATTERNS.md](LED_PATTERNS.md). Кратко:
|
||||
|
||||
| Состояние | Паттерн |
|
||||
| ---------------------- | ------------------------------------------------ |
|
||||
| Загрузчик жив, ждёт SD | Один LED: короткий импульс ~50 мс, пауза ~450 мс |
|
||||
| Приложение работает | Задаётся приложением |
|
||||
| Recovery Mode | Оба LED синхронно: 100 мс вкл / 100 мс выкл |
|
||||
|
||||
### USB (виртуальный COM-порт)
|
||||
|
||||
Загрузчик поднимает USB-порт до обращения к SD, поэтому статусы видны, даже если оператор подключился
|
||||
заранее. Обмен — текстовые JSON-строки.
|
||||
|
||||
Состояния (`status`):
|
||||
|
||||
| Значение | Когда |
|
||||
| ---------------- | ------------------------------------------------- |
|
||||
| `waiting_for_sd` | Нет валидного слота, ждём карту |
|
||||
| `installing` | Принято решение установить кандидата, идёт запись |
|
||||
| `update_skipped` | Кандидат отклонён по версии |
|
||||
| `recovery_mode` | Плата в режиме восстановления |
|
||||
|
||||
Ошибки установки: `SD_CANDIDATE_INVALID` (битый заголовок), `SD_INSTALL_WRITE_FAILED` (сбой записи),
|
||||
`SD_INSTALL_REJECTED` (записан, но подпись не прошла), `SD_DOWNGRADE_ERASE_FAILED`.
|
||||
|
||||
Статус сторожа (по запросу `wdog`): взведён ли watchdog, таймаут, был ли последний сброс по watchdog,
|
||||
текущее значение счётчика сбросов и порог.
|
||||
|
||||
---
|
||||
|
||||
## 8. Полный жизненный цикл
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
state "Загрузка" as BOOT
|
||||
state "Приложение" as APP
|
||||
state "Ожидание SD" as WAIT
|
||||
state "Установка" as INST
|
||||
state "Recovery Mode" as REC
|
||||
|
||||
[*] --> BOOT: питание
|
||||
BOOT --> APP: валидный образ выбран
|
||||
BOOT --> WAIT: нет валидного слота
|
||||
BOOT --> REC: порог сбросов / BTN 2
|
||||
WAIT --> INST: TFT_APP.BIN найден
|
||||
INST --> APP: установлено + прыжок
|
||||
INST --> WAIT: отклонено по версии/подписи
|
||||
APP --> BOOT: watchdog-сброс при зависании
|
||||
REC --> INST: образ с SD (любая подписанная версия)
|
||||
REC --> REC: ждём SD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Сводка сценариев
|
||||
|
||||
| Ситуация | Поведение загрузчика |
|
||||
| ------------------------------------------------------ | --------------------------------------------------------------------- |
|
||||
| Оба слота валидны | Загрузка слота с бо́льшей версией |
|
||||
| Валиден один слот | Загрузка его |
|
||||
| Чистая плата, оба слота пусты | Ожидание SD (бессрочно), heartbeat |
|
||||
| SD с образом новее активного | Установка в свободный слот → загрузка |
|
||||
| SD с образом старше/равным, кнопка не нажата | Пропуск, загрузка прежнего |
|
||||
| SD с образом старше, `BTN 1` удержана | Даунгрейд: установка + стирание прежнего активного → загрузка старого |
|
||||
| SD с битым заголовком / битой подписью | Отклонение, прежний валидный слот не тронут |
|
||||
| Новый образ завис, не подтвердившись (Класс A) | Watchdog-сброс → автоматический откат на прежний слот |
|
||||
| Подтверждённый образ завис, есть второй слот (Класс B) | 3 сброса → стирание зависшего слота → загрузка второго |
|
||||
| Зависает, откатываться некуда (Класс C) | Recovery Mode |
|
||||
| Оператор удержал `BTN 2` при старте (Класс D) | Recovery Mode немедленно, обычная загрузка подавлена |
|
||||
| В recovery вставлена SD с подписанным образом | Оба слота стёрты, образ в Slot A, автоматический прыжок |
|
||||
| POR после recovery без `BTN 2` | Счётчик обнулён, зависший слот пробуется заново |
|
||||
|
|
@ -84,9 +84,9 @@ typedef struct {
|
|||
|
||||
DCD содержит последовательность команд двух типов:
|
||||
|
||||
| Команда | Тег | Назначение |
|
||||
|---|---|---|
|
||||
| `Write Data` | `0xCC` | Записать значение по адресу |
|
||||
| Команда | Тег | Назначение |
|
||||
| ---------------- | ------ | -------------------------------- |
|
||||
| `Write Data` | `0xCC` | Записать значение по адресу |
|
||||
| `Check Bits Set` | `0xCF` | Ждать пока бит станет `1` (poll) |
|
||||
|
||||
Формат бинарника: `Tag(1) | Length(2 BE) | Parameter(1) | данные`.
|
||||
|
|
@ -105,11 +105,11 @@ DCD содержит последовательность команд двух
|
|||
|
||||
### 4.1 По типу образа
|
||||
|
||||
| Режим | `flags` | CSF | Применение |
|
||||
|---|---|---|---|
|
||||
| Unsigned | `0x00` | отсутствует | разработка, отладка |
|
||||
| Signed | `0x08` | RSA/ECDSA подпись | производство |
|
||||
| Signed + Encrypted | `0x0C` | подпись + шифрование кода | максимальная защита |
|
||||
| Режим | `flags` | CSF | Применение |
|
||||
| ------------------ | ------- | ------------------------- | ------------------- |
|
||||
| Unsigned | `0x00` | отсутствует | разработка, отладка |
|
||||
| Signed | `0x08` | RSA/ECDSA подпись | производство |
|
||||
| Signed + Encrypted | `0x0C` | подпись + шифрование кода | максимальная защита |
|
||||
|
||||
### 4.2 По состоянию чипа (OTP fuse)
|
||||
|
||||
|
|
@ -150,6 +150,42 @@ BootROM проверяет подпись, но **игнорирует ошиб
|
|||
→ прыгает на entry point
|
||||
```
|
||||
|
||||
### 5.1 Как это выглядит в конфиге nxpimage (`sections:` в hab_*.yaml)
|
||||
|
||||
Схема выше — идеальная production-картина с SRK-таблицей на 4 ключа. На практике `nxpimage hab export`
|
||||
собирает CSF из списка команд в `sections:` конфига — прямой аналог CSF-файла из NXP CST (Code Signing
|
||||
Tool, см. AN12263 в §10), только в YAML вместо самодельного текстового формата CST.
|
||||
|
||||
В `firmware/bootloader` сейчас (тестовый ключ, до SRK-церемонии — см. §8) используется упрощённая
|
||||
схема — **HAB4 NOCAK** («No CA Key», fast authentication): один ключ вместо иерархии SRK→CSFK/IMG.
|
||||
Пример — [tools/host/hab/hab_bootloader_release.yaml](../../tools/host/hab/hab_bootloader_release.yaml):
|
||||
|
||||
```yaml
|
||||
sections:
|
||||
- Header: {...} # версия HAB, hash-алгоритм, формат сертификата/подписи — заголовок CSF
|
||||
- InstallNOCAK: {...} # ставит ОДИН сертификат в слот 0 key store вместо SRK-таблицы
|
||||
- AuthenticateCSF: {...} # подписывает сам CSF-контейнер (иначе BootROM не станет читать остальные команды)
|
||||
- AuthenticateData: {...} # подписывает содержимое образа (IVT+BDT+код) — это и проверяет BootROM перед прыжком
|
||||
```
|
||||
|
||||
| Команда | Что делает | Ключевые поля |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
||||
| `Header` | Обязательна, идёт первой. Задаёт версию HAB (`4.2` для RT105x), алгоритм хэша (`sha256`), формат сертификата (`x509`) и формат подписи (`CMS`) — умолчания для всех следующих команд. | — |
|
||||
| `InstallNOCAK` | Устанавливает публичный сертификат (`InstallNOCAK_File`) в слот 0 внутреннего key store HAB. В полной production-схеме (диаграмма выше) это делают `InstallSRK`+`InstallCSFK` — раздельные ключи с возможностью ревокации по отдельности. NOCAK — сознательное упрощение, годится только с тестовым ключом. | `InstallNOCAK_File` — путь к `.pem`-сертификату |
|
||||
| `AuthenticateCSF` | Подписывает сам CSF-контейнер закрытым ключом — иначе ROM не станет доверять остальным командам после этой точки. Подпись создаётся на хосте во время сборки (`nxpimage`), не на чипе. | `Signer` — путь к приватному ключу |
|
||||
| `AuthenticateData` | Подписывает реальные данные образа (IVT, BDT, код) — то, что BootROM хэширует и сверяет с подписью перед прыжком на `entry point`. `AuthenticateData_VerificationIndex` должен совпадать со слотом установленного ключа (`0` для NOCAK). | `AuthenticateData_VerificationIndex`, `Signer` |
|
||||
|
||||
Ключи для тестовой подписи — [tools/host/hab/keys/](../../tools/host/hab/keys/) (пояснение там же в
|
||||
README). **Для реальной production-подписи** `flags=0x08` остаётся, но `InstallNOCAK` меняется на
|
||||
полную `InstallSRK`+`InstallCSFK` (+`InstallKey` под отдельный IMG-ключ) цепочку с настоящими SRK
|
||||
table/fuse-файлами, сгенерированными в рамках SRK-церемонии (§8) — сама YAML-механика (`nxpimage hab
|
||||
export`, `flags`, `AuthenticateData`) не меняется, меняются только сертификаты и добавляются команды
|
||||
установки промежуточных ключей.
|
||||
|
||||
Проверить, что CSF реально встроился в готовый образ: `just build::hab-verify <project> <debug|release>`
|
||||
(обёртка над `nxpimage hab parse`) — для подписанного образа поле `csf` в IVT ненулевое и рядом
|
||||
появляется отдельный `csf.bin`; для unsigned (`flags=0x00`) оба отсутствуют.
|
||||
|
||||
---
|
||||
|
||||
## 6. Почему Unsigned-образ требует HAB-контейнер
|
||||
|
|
@ -170,7 +206,7 @@ BootROM проверяет подпись, но **игнорирует ошиб
|
|||
|
||||
BootROM в режиме SDP умеет только писать в RAM и прыгать. Для записи во Flash необходим **Flashloader** — специальная программа от NXP.
|
||||
|
||||
```
|
||||
```bash
|
||||
Плата в SDP режиме (BOOT_MOD_1 = 3V3)
|
||||
│
|
||||
│ sdphost -u 0x1FC9,0x0130
|
||||
|
|
@ -181,7 +217,7 @@ BootROM в режиме SDP умеет только писать в RAM и пр
|
|||
│
|
||||
Flashloader запущен
|
||||
│ blhost -u 0x15A2,0x0073
|
||||
├─ fill-memory 0x2000 4 0xC0000007 ← option word для FlexSPI NOR
|
||||
├─ fill-memory 0x2000 4 0xC0000207 ← option word для FlexSPI NOR (+QE-бит)
|
||||
├─ configure-memory 9 0x2000 ← Flashloader пишет FCB в Flash
|
||||
├─ flash-erase-region 0x60000000 …
|
||||
├─ write-memory 0x60002000 firmware_hab.bin ← HAB образ
|
||||
|
|
@ -192,12 +228,12 @@ BootROM в режиме SDP умеет только писать в RAM и пр
|
|||
|
||||
## 8. Жизненный цикл для проекта TFT
|
||||
|
||||
| Стадия | Режим HAB | Подпись | Fuse |
|
||||
|---|---|---|---|
|
||||
| Разработка | Open | Unsigned (`0x00`) | не трогаем |
|
||||
| Входной контроль | Open | Unsigned (`0x00`) | не трогаем |
|
||||
| Предсерийные образцы | Open | Signed (`0x08`) | не трогаем |
|
||||
| Серийное производство | **Closed** | Signed (`0x08`) | `SEC_CONFIG = 1` |
|
||||
| Стадия | Режим HAB | Подпись | Fuse |
|
||||
| --------------------- | ---------- | ----------------- | ---------------- |
|
||||
| Разработка | Open | Unsigned (`0x00`) | не трогаем |
|
||||
| Входной контроль | Open | Unsigned (`0x00`) | не трогаем |
|
||||
| Предсерийные образцы | Open | Signed (`0x08`) | не трогаем |
|
||||
| Серийное производство | **Closed** | Signed (`0x08`) | `SEC_CONFIG = 1` |
|
||||
|
||||
> ⚠️ Запись `SEC_CONFIG = 1` необратима. Перед закрытием HAB необходимо убедиться, что подписанный образ успешно проходит верификацию на реальном железе.
|
||||
|
||||
|
|
@ -205,13 +241,13 @@ BootROM в режиме SDP умеет только писать в RAM и пр
|
|||
|
||||
## 9. Инструменты
|
||||
|
||||
| Инструмент | Назначение |
|
||||
|---|---|
|
||||
| `nxpimage hab export` | Сборка HAB-образа (IVT + BDT + DCD + опционально CSF) |
|
||||
| `nxpimage hab parse` | Разбор готового образа для проверки |
|
||||
| `sdphost` | Связь с BootROM в SDP режиме (запись в RAM, прыжок) |
|
||||
| `blhost` | Связь с Flashloader (запись во Flash, конфигурация памяти) |
|
||||
| `nxpdevscan` | Обнаружение подключённых NXP устройств |
|
||||
| Инструмент | Назначение |
|
||||
| --------------------- | ---------------------------------------------------------- |
|
||||
| `nxpimage hab export` | Сборка HAB-образа (IVT + BDT + DCD + опционально CSF) |
|
||||
| `nxpimage hab parse` | Разбор готового образа для проверки |
|
||||
| `sdphost` | Связь с BootROM в SDP режиме (запись в RAM, прыжок) |
|
||||
| `blhost` | Связь с Flashloader (запись во Flash, конфигурация памяти) |
|
||||
| `nxpdevscan` | Обнаружение подключённых NXP устройств |
|
||||
|
||||
---
|
||||
|
||||
11
docs/bootloader/LED_PATTERNS.md
Normal file
11
docs/bootloader/LED_PATTERNS.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# LED-индикация bootloader
|
||||
|
||||
Два светодиода: **HEARTBEAT** (системный) и **APP** (прикладной).
|
||||
|
||||
| Индикация | Значение |
|
||||
| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| HEARTBEAT мигает: 50 мс горит / 450 мс не горит. APP не горит вообще. | Загрузчик работает, ждёт карту microSD |
|
||||
| HEARTBEAT мигает: 50 мс горит / 450 мс не горит. APP мигает: 250 мс горит / 250 мс не горит. | Идёт установка образа с SD-карты — питание не выключать |
|
||||
| HEARTBEAT мигает: 50 мс горит / 450 мс не горит. APP часто мигает без остановки: 100 мс горит / 100 мс не горит. | Неисправность платы (QSPI flash или SDRAM) — плата не годна, отложить |
|
||||
| APP быстро мигает 4 раза подряд (80 мс горит / 80 мс не горит), затем возвращается к тому виду, что был до этого. | Образ на SD-карте отклонён (битый файл или неверная подпись) — заменить файл на карте |
|
||||
| Оба светодиода горят и гаснут одновременно: 100 мс горят / 100 мс не горят. | Режим восстановления (Recovery Mode) |
|
||||
138
docs/mimxrt1052/UPDATE_FLOW.md
Normal file
138
docs/mimxrt1052/UPDATE_FLOW.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# UPDATE_FLOW.md — производственная заливка и полевое обновление tft_app
|
||||
|
||||
Разбор «что и куда заливать» для связки **bootloader → tft_app** (MCUboot Direct-XIP, два слота A/Б).
|
||||
Дополняет [BOOTLOADER_FLASH_MAP.md](BOOTLOADER_FLASH_MAP.md) (адреса) и
|
||||
[../../firmware/bootloader/PLAN.md](../../firmware/bootloader/PLAN.md) (фазы, recovery).
|
||||
|
||||
---
|
||||
|
||||
## TL;DR (главное, что вызывает непонимание)
|
||||
|
||||
**Direct-XIP: образ исполняется прямо из адреса своего слота, код обычно НЕ позиционно-независим →
|
||||
под каждый слот нужна СВОЯ линковка.** Обновление, которое встанет в Slot Б, должно быть слинковано
|
||||
под адрес Slot Б (`0x60240000`). Поэтому:
|
||||
|
||||
> **Релиз одной версии tft_app = ДВА подписанных бинаря** (линковка под A + линковка под Б), из
|
||||
> ОДНОГО исходника, параметризованным линкер-скриптом — ровно как уже устроен `test_stub`
|
||||
> (`--defsym=__slot_base__=…`). Версия (`-v X.Y.Z`) одинаковая в обоих.
|
||||
|
||||
Да, «собрать новую версию со своим линкер-скриптом под слот Б» — так и есть. Но не вручную по одному:
|
||||
это один параметризованный `.ld` и один рецепт сборки, дающий оба бинаря.
|
||||
|
||||
---
|
||||
|
||||
## 1. Почему так — это свойство Direct-XIP, не наша прихоть
|
||||
|
||||
- **XIP** = eXecute In Place: CPU фетчит инструкции напрямую из флеша по адресу слота, образ никуда не
|
||||
копируется (в отличие от swap/scratch-режимов MCUboot, которые мы сознательно НЕ используем).
|
||||
- Абсолютные адреса — vector table, указатели на функции, литеральные пулы, адрес инициализации
|
||||
`.data` — фиксируются на этапе **линковки** под конкретный базовый адрес. Образ, слинкованный под
|
||||
Slot A (`0x60040000`), в Slot Б (`0x60240000`) поедет по чужим адресам и не запустится корректно.
|
||||
- Это прямо зафиксированный «открытый вопрос §4» в [BOOTLOADER_FLASH_MAP.md](BOOTLOADER_FLASH_MAP.md):
|
||||
*«код обычно не позиционно-независим — вероятно потребуется два варианта линковки под Slot A и
|
||||
Slot Б, либо PIC»*. `test_stub` (Фаза 2) уже подтвердил two-slot-two-linkage на реальном железе.
|
||||
|
||||
> ⚠️ **Почему на `test_stub` «одинаковый» бинарь как будто работал в обоих слотах.** Заглушка
|
||||
> крошечная (только мигание LED): почти весь её код PC-relative, `.data` минимальна, а vector table
|
||||
> релоцируется и `boot_select` (ставит `VTOR`), и самим образом в старте — поэтому она позиционно
|
||||
> **терпима** по случайности. Реальный tft_app (большой, с `.data`, абсолютными указателями,
|
||||
> шрифтами-C-массивами, framebuffer) терпимым **не будет** — ему нужны обе линковки по-настоящему.
|
||||
|
||||
---
|
||||
|
||||
## 2. Карта слотов (напоминание)
|
||||
|
||||
| Слот | База | Размер | ORIGIN образа (после imgtool-заголовка `0x200`) |
|
||||
|---|---|---|---|
|
||||
| Slot A | `0x60040000` | 2 МБ | `0x60040200` |
|
||||
| Slot Б | `0x60240000` | 2 МБ | `0x60240200` |
|
||||
|
||||
Линкер tft_app: `ORIGIN = __slot_base__ + 0x200` (образец — `cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld`
|
||||
у `test_stub`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Релизные артефакты (на каждую версию)
|
||||
|
||||
Из одного исходника tft_app — **два подписанных бинаря** production-ключом:
|
||||
|
||||
| Файл | Линковка (`__slot_base__`) | Назначение |
|
||||
|---|---|---|
|
||||
| `tft_app_slotA_vX.Y.Z.signed.bin` | `0x60040000` | ставится в Slot A |
|
||||
| `tft_app_slotB_vX.Y.Z.signed.bin` | `0x60240000` | ставится в Slot Б |
|
||||
|
||||
- Оба — **одна версия** `-v X.Y.Z` (version-gate сравнивает версию образа, а не его линковку).
|
||||
- Оба подписаны production-ключом (не тестовым sample-ключом MCUboot; см. HAB SRK-церемонию как
|
||||
аналог для bootloader).
|
||||
- Рецепт — по образцу `just build::build-mcuboot-stub` (собирает оба слота + подписывает imgtool'ом),
|
||||
но с production-ключом и реальным tft_app вместо заглушки.
|
||||
|
||||
---
|
||||
|
||||
## 4. Кейсы
|
||||
|
||||
| # | Ситуация | Куда/что | Как |
|
||||
|---|---|---|---|
|
||||
| 1 | **Начальная production-заливка** | A-линковка → Slot A; Slot Б пуст | SWD / USB ROM (blhost), `just host::flash-production`-путь |
|
||||
| 2 | **Первое полевое обновление** v1→v2 | A активен → target = Slot Б → ставится **Б-линковка** v2 | SD, авто по version-gate; после — A=v1 (fallback), Б=v2 (active) |
|
||||
| 3 | **Следующее обновление** v2→v3 | Б активен → target = Slot A → ставится **A-линковка** v3 | SD; после — A=v3 (active), Б=v2 (fallback) |
|
||||
| 4 | **Даунгрейд** (BTN_1 при старте) | старая версия в inactive + стирание прежнего активного | SD + удержание BTN_1 (см. Фаза 3) |
|
||||
| 5 | **Recovery** (BTN_2 / class C) | оба слота стёрты → чистый борт → A-линковка в Slot A | SD, ослабленный version-gate (см. Фаза 6) |
|
||||
| 6 | **Образ завис после обновления** | авто-откат (class A) или счётчик+фолбэк (class B) | см. Фаза 6, taxonomy A/B |
|
||||
|
||||
**Ключевой инвариант alternation A/Б:** обновление всегда идёт в НЕактивный слот, прежний рабочий слот
|
||||
остаётся нетронутым как fallback (см. `update_policy` — «целевой слот всегда НЕ активный»). Поэтому в
|
||||
поле почти всегда есть куда откатиться, если новая версия окажется плохой.
|
||||
|
||||
---
|
||||
|
||||
## 5. Как обновление выбирает нужную линковку — ТЕКУЩИЙ GAP
|
||||
|
||||
**Сейчас** ([sd_update.c](../../firmware/bootloader/src/sd_update.c)): ищется ОДИН файл
|
||||
`2:/TFT_APP.BIN` и ставится в неактивный слот (`decision.target_slot`). Для позиционно-терпимого
|
||||
`test_stub` это прошло все 5 сценариев Фазы 3 — но для реального tft_app **сломается**: если активен
|
||||
Slot A, обновление идёт в Slot Б, а `TFT_APP.BIN` мог быть слинкован под A → в Slot Б не запустится.
|
||||
|
||||
**Нужное расширение (часть включения реального tft_app):**
|
||||
- SD несёт **оба** линкованных бинаря по соглашению имён, напр. `TFT_APP_A.BIN` / `TFT_APP_B.BIN`.
|
||||
- `sd_update` открывает файл **по целевому слоту**: `TFT_APP_A.BIN`, если target = Slot A, иначе
|
||||
`TFT_APP_B.BIN`.
|
||||
- Оператор кладёт на SD **оба** и не думает, какой слот сейчас активен — bootloader сам берёт нужный
|
||||
под инактивный слот.
|
||||
- Version-gate: bootloader читает версию из выбранного файла (в обоих одна) — сравнение корректно.
|
||||
|
||||
Это простое изменение (одна развилка имени файла по `target_slot`), но его **надо сделать до первого
|
||||
реального полевого обновления tft_app**. Пока стоит `test_stub` — не мешает.
|
||||
|
||||
---
|
||||
|
||||
## 6. Альтернатива — PIC (почему не сейчас)
|
||||
|
||||
Позиционно-независимый образ (один бинарь на оба слота) — теоретически убирает дублирование линковки.
|
||||
Но на bare-metal Cortex-M это **ROPI/RWPI**: флаги компилятора, PI-совместимый startup, `r9` как
|
||||
static base для RW-данных, и **все** библиотеки (FreeRTOS, SDK-драйверы, шрифты) собранные в PI-режиме,
|
||||
плюс runtime-оверхед. Объём работы и риск большие; для Direct-XIP индустрия стандартно выбирает
|
||||
dual-link. Оставляем PIC на «если поддержка двух сборок станет реальной обузой» — не сейчас.
|
||||
|
||||
---
|
||||
|
||||
## 7. Конкретные TODO для включения реального tft_app (сводка)
|
||||
|
||||
1. **Параметризованный `.ld`** tft_app под два адреса слота (образец — `test_stub`
|
||||
`MIMXRT1052xxxxx_mcuboot_slot.ld` + `--defsym=__slot_base__=`).
|
||||
2. **Рецепт сборки+подписи** двух бинарей на релиз (по образцу `build-mcuboot-stub`, но production-ключ).
|
||||
3. **Расширить `sd_update`**: `TFT_APP_<A|B>.BIN` по `decision.target_slot` (§5).
|
||||
4. **Контракт tft_app** (Фаза 6, 6c): обслуживание WDOG (alive-flag), отложенный `boot_set_confirmed()`,
|
||||
`bsp_boot_health_mark()`.
|
||||
5. **Решить**: production заливать только Slot A или A+Б? Достаточно A — первое обновление наполнит Б
|
||||
и даст fallback *другой* версии (A+Б одной версией от class-B не спасает — тот же баг в обоих).
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [BOOTLOADER_FLASH_MAP.md](BOOTLOADER_FLASH_MAP.md) — адреса слотов, обоснование размеров.
|
||||
- [../../firmware/bootloader/PLAN.md](../../firmware/bootloader/PLAN.md) — фазы; Фаза 6 (recovery,
|
||||
taxonomy зависаний), Фаза 5 (HAB/production/service-tui).
|
||||
- [HAB_GUIDE.md](HAB_GUIDE.md) — подпись самого bootloader (HAB), не путать с imgtool-подписью образов
|
||||
tft_app.
|
||||
|
|
@ -4,7 +4,13 @@
|
|||
>
|
||||
> Документ описывает протокол обмена между диагностической прошивкой
|
||||
> (`firmware_test`) и хостовым ПО сервисного инженера.
|
||||
> Актуален для: `firmware_test v0.1.0+`, `protocol.h v2`.
|
||||
> Актуален для: `firmware_test v0.1.2+`, `protocol.h v2`.
|
||||
>
|
||||
> Полный справочник по каждому тесту (потоки, коды `detail`, таблица HIL
|
||||
> реле) — в [firmware/test/README.md](../../firmware/test/README.md) и
|
||||
> [firmware/test/src/tests/README.md](../../firmware/test/src/tests/README.md).
|
||||
> Этот документ — сжатый протокольный обзор с точки зрения хостового ПО
|
||||
> (TUI/pytest), а не полное описание тест-логики.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -79,7 +85,7 @@ sequenceDiagram
|
|||
participant T as Таргет
|
||||
|
||||
Note over T: прошивка загружена через USB SDP
|
||||
T-->>H: {"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
|
||||
T-->>H: {"type":"session_start","fw":"0.1.2","target":"IMXRT1052","uptime_ms":0}
|
||||
|
||||
H->>T: {"type":"cmd","cmd":"ping"}
|
||||
T-->>H: {"type":"pong"}
|
||||
|
|
@ -169,6 +175,17 @@ sequenceDiagram
|
|||
← {"ok":false,"error":"UID_READ_ERR"}
|
||||
```
|
||||
|
||||
### `get_version` — чтение версии прошивки
|
||||
|
||||
```json
|
||||
→ {"type":"cmd","cmd":"get_version"}
|
||||
← {"type":"version_response","fw":"0.1.2"}
|
||||
```
|
||||
|
||||
Дублирует значение `"fw"` из `session_start` — полезно, если хост
|
||||
подключился уже после того, как `session_start` был отправлен (может быть
|
||||
пропущен, это одноразовое событие сразу после старта).
|
||||
|
||||
### `run_selected` — запуск подмножества тестов
|
||||
|
||||
Запускает тесты по списку ID. Порядок выполнения — по реестру таргета,
|
||||
|
|
@ -199,7 +216,7 @@ sequenceDiagram
|
|||
### `session_start`
|
||||
|
||||
```json
|
||||
{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
|
||||
{"type":"session_start","fw":"0.1.2","target":"IMXRT1052","uptime_ms":0}
|
||||
```
|
||||
|
||||
### `test_begin`
|
||||
|
|
@ -222,6 +239,11 @@ sequenceDiagram
|
|||
|
||||
`detail` — ASCII-строка до 95 символов. При `pass` — пустая.
|
||||
|
||||
### `test_list`
|
||||
|
||||
Ответ на `list_tests` — массив дескрипторов теста (`id`, `name`,
|
||||
`critical`, `requires_hil`), см. пример в разделе `list_tests` выше.
|
||||
|
||||
### `progress`
|
||||
|
||||
```json
|
||||
|
|
@ -230,6 +252,11 @@ sequenceDiagram
|
|||
|
||||
Промежуточные шаги внутри теста. Используется в `usd`.
|
||||
|
||||
### `uid_response` / `version_response`
|
||||
|
||||
Ответы на `get_uid`/`get_version` — см. описание соответствующих команд
|
||||
выше.
|
||||
|
||||
### `confirm_request`
|
||||
|
||||
```json
|
||||
|
|
@ -270,25 +297,30 @@ sequenceDiagram
|
|||
| `UNKNOWN_TEST` | Поле `"id"` в `run` или `"tests"` в `run_selected` содержит неизвестный ID |
|
||||
| `LINE_TOO_LONG` | Входящая строка превысила 128 байт |
|
||||
| `BUSY` | Таргет выполняет тест, новая команда отклонена |
|
||||
| `UID_READ_ERR` | `bsp_prov_read_uid()` вернул ошибку (ответ на `get_uid`) |
|
||||
|
||||
---
|
||||
|
||||
## Матрица тестов
|
||||
|
||||
| ID | Название | Тип | Critical | HIL (M5) | Интерактивный |
|
||||
| ---------- | --------------------- | ------------------ | -------- | -------- | ------------------ |
|
||||
| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
|
||||
| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ |
|
||||
| `usd` | uSD (SDIO) | self + interactive | ❌ | ❌ | ✅ (вставить карту) |
|
||||
| `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ (цвета R/G/B/W) |
|
||||
| `buttons` | Кнопки Test_But_1/2 | interactive | ❌ | ❌ | ✅ (нажать кнопки) |
|
||||
| `can` | CAN | HIL | ❌ | ✅ | ❌ |
|
||||
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ |
|
||||
| `uart_iso` | UART ISO / RS_RX Opto | HIL | ❌ | ✅ | ❌ |
|
||||
| `opto` | Opto-in EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ |
|
||||
Порядок — как в реестре `k_registry[]` (`test_runner.c`); полная версия с
|
||||
кодами `detail` и HIL-таблицей реле — в
|
||||
[firmware/test/README.md §Матрица тестов](../../firmware/test/README.md#матрица-тестов).
|
||||
|
||||
| ID | Название | Тип | Critical | HIL (M5) | Интерактивный |
|
||||
| --------- | ------------------- | ------------------ | -------- | -------- | --------------------- |
|
||||
| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
|
||||
| `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ |
|
||||
| `usd` | microSD (SDIO) | self + interactive | ❌ | ❌ | ✅ (вставить карту) |
|
||||
| `display` | TFT Display RGB888 | interactive | ❌ | ❌ | ✅ (6 шагов, см. ниже) |
|
||||
| `buttons` | Test Buttons | interactive | ❌ | ❌ | ✅ (нажать кнопки) |
|
||||
| `opto` | Opto Inputs | HIL | ❌ | ✅ | ❌ (авто, 6 шагов) |
|
||||
| `can` | CAN loopback | HIL | ❌ | ✅ | ❌ (авто, 2 шага) |
|
||||
| `mqs` | MQS Audio Out | interactive | ❌ | ❌ | ✅ (слышимость тона) |
|
||||
|
||||
**Типы:** **self** — таргет тестирует периферию самостоятельно; **interactive** —
|
||||
требует `confirm_request`; **HIL** — требует M5StampPLC.
|
||||
требует `confirm_request`, отвечает оператор; **HIL** — требует M5StampPLC,
|
||||
confirm автоматический (без оператора).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -320,6 +352,10 @@ sequenceDiagram
|
|||
|
||||
### Display (RGB888)
|
||||
|
||||
Шесть шагов: Red → Green → Blue → White, затем два ротационных (диагностика
|
||||
непропаянных LR/UD пинов на TFT7/8/10). Тест прерывается на **первом**
|
||||
неподтверждённом шаге.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant H as Хост
|
||||
|
|
@ -333,10 +369,36 @@ sequenceDiagram
|
|||
T-->>H: {"type":"confirm_request","id":"display_blue","prompt":"Экран залит синим?","timeout_ms":15000}
|
||||
H->>T: {"type":"confirm","id":"display_blue","confirmed":true}
|
||||
T-->>H: {"type":"confirm_request","id":"display_white","prompt":"Экран залит белым?","timeout_ms":15000}
|
||||
H->>T: {"type":"confirm","id":"display_white","confirmed":false}
|
||||
T-->>H: {"type":"test_result","id":"display","status":"fail","ms":22103,"detail":"display_white not confirmed"}
|
||||
H->>T: {"type":"confirm","id":"display_white","confirmed":true}
|
||||
T-->>H: {"type":"confirm_request","id":"display_rot0","prompt":"Слева КРАСНЫЙ, справа СИНИЙ?","timeout_ms":15000}
|
||||
H->>T: {"type":"confirm","id":"display_rot0","confirmed":true}
|
||||
T-->>H: {"type":"confirm_request","id":"display_rot_base","prompt":"Красный/синий поменялись сторонами?","timeout_ms":15000}
|
||||
H->>T: {"type":"confirm","id":"display_rot_base","confirmed":true}
|
||||
T-->>H: {"type":"test_result","id":"display","status":"pass","ms":42310,"detail":""}
|
||||
```
|
||||
|
||||
При отказе/таймауте на любом шаге: `status:"fail"`,
|
||||
`detail:"<id> not confirmed"` (например, `"display_white not confirmed"`).
|
||||
|
||||
### MQS Audio Out
|
||||
|
||||
Таргет ~4с играет мелодию через MQS + усилитель, затем запрашивает
|
||||
подтверждение слышимости — единственный тест с аудио-confirm:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant H as Хост
|
||||
participant T as Таргет
|
||||
|
||||
T-->>H: {"type":"test_begin","id":"mqs",...}
|
||||
Note over T: ~4с воспроизведение тона (A4, затем E5)
|
||||
T-->>H: {"type":"confirm_request","id":"mqs_tone","prompt":"Do you hear a tone?","timeout_ms":15000}
|
||||
H->>T: {"type":"confirm","id":"mqs_tone","confirmed":true}
|
||||
T-->>H: {"type":"test_result","id":"mqs","status":"pass","ms":19240,"detail":""}
|
||||
```
|
||||
|
||||
Отказ/таймаут → `status:"fail"`, `detail:"operator: no sound"`.
|
||||
|
||||
### Кнопки
|
||||
|
||||
```mermaid
|
||||
|
|
@ -393,32 +455,15 @@ firmware/test/src/
|
|||
├── test_usd.c
|
||||
├── test_display.c
|
||||
├── test_buttons.c
|
||||
├── test_opto.c
|
||||
├── test_can.c
|
||||
├── test_uart_ttl.c
|
||||
├── test_uart_iso.c
|
||||
└── test_opto.c
|
||||
└── test_mqs.c
|
||||
```
|
||||
|
||||
### Добавление нового теста
|
||||
|
||||
1. Создать `firmware/test/src/tests/test_foo.c`.
|
||||
2. Объявить дескриптор:
|
||||
|
||||
```c
|
||||
const test_module_t k_test_foo = {
|
||||
.id = "foo",
|
||||
.name = "Foo Peripheral",
|
||||
.critical = false,
|
||||
.requires_hil = false,
|
||||
.pre_confirm_prompt = NULL,
|
||||
.init = NULL,
|
||||
.run = test_foo_run,
|
||||
.deinit = NULL,
|
||||
};
|
||||
```
|
||||
|
||||
1. Добавить `&k_test_foo` в реестр `test_runner.c`.
|
||||
2. Добавить `tests/test_foo.c` в `CMakeLists.txt` таргета.
|
||||
Пошаговый гайд с шаблонами (self-тест, интерактивный, pre-confirm) —
|
||||
[firmware/test/README.md §Как добавить новый тест](../../firmware/test/README.md#как-добавить-новый-тест).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -166,12 +166,18 @@ ls build/target-debug/tests/target/<name>/test_<name>.elf
|
|||
|
||||
## Шаг 5 — `conftest.py`: добавить фикстуры
|
||||
|
||||
### Базовый тест (без M5)
|
||||
### Любой тест — фикстура загрузки всегда зависит от `m5`
|
||||
|
||||
M5StampPLC управляет питанием таргета (RLY1 → VIN, см. `HIL_BENCH.md`), а
|
||||
не только сигнальными реле — поэтому `loaded_<n>` зависит от `m5` **во всех
|
||||
случаях**, даже если сам тест не использует реле для сигналов (например,
|
||||
`01_test_uart.py`/`loaded_host_uart`). Без этой зависимости pyOCD попытается
|
||||
подключиться к обесточенной плате.
|
||||
|
||||
```python
|
||||
# 1. Фикстура загрузки
|
||||
# 1. Фикстура загрузки — m5 гарантирует, что питание включено до pyOCD
|
||||
@pytest.fixture(scope="module")
|
||||
def loaded_<n>(request: pytest.FixtureRequest) -> None:
|
||||
def loaded_<n>(request: pytest.FixtureRequest, m5: M5Agent) -> None:
|
||||
_load_elf(
|
||||
request,
|
||||
Path(cfg.BUILD_DIR) / "tests/target/<n>/test_<n>.elf",
|
||||
|
|
@ -184,26 +190,10 @@ _UART_FIXTURE_MAP = {
|
|||
}
|
||||
```
|
||||
|
||||
### Тест с M5
|
||||
|
||||
```python
|
||||
# 1. Зависимость от m5 гарантирует что питание включено до загрузки ELF
|
||||
@pytest.fixture(scope="module")
|
||||
def loaded_<n>(request: pytest.FixtureRequest, m5: M5Agent) -> None:
|
||||
_load_elf(
|
||||
request,
|
||||
Path(cfg.BUILD_DIR) / "tests/target/<n>/test_<n>.elf",
|
||||
)
|
||||
|
||||
# 2. UART-фикстура — та же одна строка
|
||||
_UART_FIXTURE_MAP = {
|
||||
...
|
||||
"uart_<n>": "loaded_<n>",
|
||||
}
|
||||
```
|
||||
|
||||
**Правило:** если тест управляет железом через M5 — `loaded_<n>` должен явно
|
||||
зависеть от `m5`, иначе pyOCD попытается подключиться до включения питания.
|
||||
Различие между «базовым» и «с M5» тестом — не в сигнатуре `loaded_<n>`
|
||||
(она всегда одна и та же), а в том, использует ли сам **тест-кейс**
|
||||
`m5.opto_set()`/`m5.relay_set()`/`m5.can_*()` для управления сигналами
|
||||
помимо включения питания (см. пример «Тест с M5» в Шаге 6 ниже).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,232 +1,193 @@
|
|||
# Отладка прошивок через SWD + GDB
|
||||
# Добавление нового host unit-теста
|
||||
|
||||
## Обзор архитектуры
|
||||
Пошаговый гайд для разработчика. Полный справочник по Unity/FFF API,
|
||||
структуре stub-хедеров и типичным ловушкам — в
|
||||
[tests/host/README.md](../../../tests/host/README.md). Этот документ —
|
||||
только про шаги добавления нового теста в сборку.
|
||||
|
||||
Отладка построена на пробросе GDB-сервера с хоста в devcontainer по TCP. Это
|
||||
позволяет держать весь инструментарий сборки и языковой сервер внутри контейнера,
|
||||
не проводя USB-пробник внутрь Docker.
|
||||
---
|
||||
|
||||
## Обзор стека
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Host["Хост (macOS / Linux)"]
|
||||
DS["just host::debug-server\npyocd gdbserver :3333"]
|
||||
ML["MCU-Link (CMSIS-DAP)"]
|
||||
DS --> ML
|
||||
subgraph DC["Devcontainer (единственное место запуска)"]
|
||||
C["tests/host/<dir>/test_<name>.c\nUnity [+ fff]"]
|
||||
CP["CMakePresets.json\nhost-debug / host-release"]
|
||||
JB["just/build.just\ntest-host"]
|
||||
C --> CP --> JB
|
||||
end
|
||||
|
||||
subgraph DC["Devcontainer"]
|
||||
CD["cortex-debug\n(VSCode F5)"]
|
||||
GDB["arm-none-eabi-gdb\nсимволы из .elf"]
|
||||
CD --> GDB
|
||||
end
|
||||
|
||||
Board["MIMXRT1052\nFlash / SDRAM\nSEGGER RTT буфер"]
|
||||
|
||||
GDB -->|"TCP host.docker.internal:3333"| DS
|
||||
ML -->|"SWD"| Board
|
||||
```
|
||||
|
||||
**Ключевой принцип:** `pyocd gdbserver` слушает на `0.0.0.0:3333`. Из контейнера
|
||||
GDB подключается через `host.docker.internal:3333` — специальный DNS-алиас Docker,
|
||||
резолвится в IP хост-машины.
|
||||
Host-тесты компилируются `clang-17` **на хосте** (не ARM GCC), исполняются
|
||||
как обычные нативные бинарники под `ctest`. Никакого железа не требуется —
|
||||
в отличие от HIL-тестов (см. [../hil/HIL_CREATE_TEST.md](../hil/HIL_CREATE_TEST.md)).
|
||||
|
||||
---
|
||||
|
||||
## Компоненты
|
||||
## Шаг 0 — Определить категорию модуля
|
||||
|
||||
### На хосте
|
||||
| Категория | Инструментарий | Пример |
|
||||
| ----------------------------------------- | ------------------ | ----------------------------------- |
|
||||
| **A** — платформонезависимый | Только Unity | `protocol.c`, `test_runner.c`, `ring_buffer.c` |
|
||||
| **B** — BSP-модуль (зависит от NXP SDK) | Unity + fff + stub-хедеры | `bsp/led`, `bsp/opto`, `bsp/can`, `bsp/button` |
|
||||
|
||||
| Компонент | Роль | Источник |
|
||||
| --------------------------------- | ------------------------------- | -------------------------- |
|
||||
| `pyocd` | GDB-сервер + flash-программатор | `tools/hil/uv.lock` |
|
||||
| `MCU-Link` | CMSIS-DAP v2 пробник | USB к плате |
|
||||
| `just host::debug-server` | Запуск GDB-сервера | `just/host.just` |
|
||||
| `just host::flash-swd-*` | Прошивка через SWD | `just/host.just` |
|
||||
| `tools/host/flash_swd.py` | Сборка FCB+HAB образа и запись | `tools/host/` |
|
||||
| `tools/host/dcd/w25q128_fdcb.bin` | FCB для W25Q128 (Quad SPI) | NXP SecureProvisioningTool |
|
||||
|
||||
### В devcontainer
|
||||
|
||||
| Компонент | Роль |
|
||||
| -------------------------------------- | ---------------------------------------------- |
|
||||
| `arm-none-eabi-gdb` | GDB клиент, подключается к серверу на хосте |
|
||||
| `cortex-debug` (VSCode extension) | UI для GDB: брейкпоинты, стек, регистры |
|
||||
| `.vscode/launch.json` | Конфигурации запуска отладки |
|
||||
| `.vscode/tasks.json` | `preLaunchTask` — пересборка ELF перед стартом |
|
||||
| `build/Debug/*.elf` | Символы для GDB (DWARF debug info) |
|
||||
| `bsp/generated/startup/MIMXRT1052.xml` | SVD — описание регистров периферии |
|
||||
|
||||
### Конфигурация
|
||||
|
||||
Параметры отладки задаются в `.env`:
|
||||
|
||||
```bash
|
||||
GDB_PORT=3333
|
||||
PYOCD_TARGET=mimxrt1050_quadspi
|
||||
PYOCD_FREQUENCY=4000000
|
||||
FCB_PATH=tools/host/dcd/w25q128_fdcb.bin
|
||||
```
|
||||
Полное объяснение разницы и структуры — в
|
||||
[tests/host/README.md §1](../../../tests/host/README.md#1-две-категории-тестируемых-модулей).
|
||||
|
||||
---
|
||||
|
||||
## Поддерживаемые прошивки
|
||||
|
||||
| Конфигурация VSCode | ELF | Особенности |
|
||||
| ----------------------------- | ------------------------------- | ---------------------------- |
|
||||
| `🐛 Debug: firmware_test` | `build/Debug/firmware_test.elf` | Bare-metal, входной контроль |
|
||||
| `🐛 Debug: bootloader` | `build/Debug/bootloader.elf` | Bare-metal, A/B обновление |
|
||||
| `🐛 Debug: tft_app (FreeRTOS)` | `build/Debug/app.elf` | FreeRTOS, task view |
|
||||
|
||||
Все три — XIP-прошивки, исполняются из QuadSPI NOR Flash (`0x60000000`).
|
||||
|
||||
---
|
||||
|
||||
## Режимы запуска отладки
|
||||
|
||||
### Режим А — прошивка уже в Flash
|
||||
## Шаг 1 — Создать тестовый файл
|
||||
|
||||
```bash
|
||||
# 1. Хост — запустить GDB-сервер (оставить в отдельном терминале)
|
||||
just host::debug-server
|
||||
|
||||
# 2. DevContainer — VSCode
|
||||
# Run & Debug (Ctrl+Shift+D) → выбрать конфигурацию → F5
|
||||
mkdir -p tests/host/<name>/
|
||||
touch tests/host/<name>/test_<name>.c
|
||||
```
|
||||
|
||||
GDB сбрасывает MCU, загружает символы из ELF и останавливается на входе
|
||||
в `main`. Flash не перезаписывается.
|
||||
|
||||
### Режим Б — прошить через SWD, затем отладить
|
||||
|
||||
```bash
|
||||
# 1. DevContainer
|
||||
just build::hab-firmware-test-debug
|
||||
|
||||
# 2. Хост
|
||||
just host::flash-swd-test-debug
|
||||
|
||||
# 3. ⚡ Power cycle платы (обязательно)
|
||||
|
||||
# 4. Хост
|
||||
just host::debug-server
|
||||
|
||||
# 5. DevContainer — VSCode → 🐛 Debug: firmware_test → F5
|
||||
```
|
||||
|
||||
### Режим В — прошить через USB SDP, затем отладить
|
||||
|
||||
```bash
|
||||
# 1. DevContainer
|
||||
just build::build-firmware-test-debug
|
||||
|
||||
# 2. Хост — перевести плату в SDP-режим, затем:
|
||||
just host::flash-test-debug
|
||||
|
||||
# 3. Хост
|
||||
just host::debug-server
|
||||
|
||||
# 4. DevContainer — VSCode → 🐛 Debug: firmware_test → F5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Почему flash через SWD требует FCB
|
||||
|
||||
При USB SDP ROM-загрузчик инициализирует FlexSPI по DCD из HAB-образа — FCB
|
||||
не нужен. При SWD flash-алгоритм pyOCD пишет в NOR Flash напрямую. При
|
||||
cold-start Boot ROM сначала читает FCB по адресу `0x60000000`, конфигурирует
|
||||
FlexSPI, и только потом ищет IVT. Без FCB бутлоадер не стартует.
|
||||
|
||||
`flash_swd.py` решает это, собирая образ перед записью:
|
||||
|
||||
```bash
|
||||
0x60000000 w25q128_fdcb.bin (512 байт) — FCB
|
||||
0x60000200 0xFF × 3584 байт — padding
|
||||
0x60001000 firmware_test_hab.bin — IVT + DCD + код
|
||||
```
|
||||
|
||||
Весь диапазон `0x60000000–0x6000FFFF` — один 64KB сектор: стирается и
|
||||
записывается за одну транзакцию.
|
||||
|
||||
---
|
||||
|
||||
## RTT-логи
|
||||
|
||||
SEGGER RTT включён только в Debug-сборках (`SEGGER_RTT_ENABLED=ON`).
|
||||
После старта отладки вкладка `TERMINAL → RTT` принимает вывод канала 0.
|
||||
`cortex-debug` находит адрес буфера по символу `_SEGGER_RTT` из ELF.
|
||||
### Шаблон — категория A (без моков)
|
||||
|
||||
```c
|
||||
#include "SEGGER_RTT.h"
|
||||
SEGGER_RTT_printf(0, "value = %d\n", value);
|
||||
#include "unity.h"
|
||||
#include "<модуль>.h" /* тестируемый модуль */
|
||||
|
||||
void setUp(void) { /* сброс состояния если нужен */ }
|
||||
void tearDown(void) { }
|
||||
|
||||
void test_something(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL(expected, actual);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_something);
|
||||
return UNITY_END();
|
||||
}
|
||||
```
|
||||
|
||||
### Шаблон — категория B (с fff-фейками)
|
||||
|
||||
```c
|
||||
#include "unity.h"
|
||||
#include "fff.h"
|
||||
|
||||
DEFINE_FFF_GLOBALS; /* ровно один раз на файл */
|
||||
|
||||
/* 1. Stub-хедер с типами NXP SDK */
|
||||
#include "fsl_gpio.h"
|
||||
|
||||
/* 2. Фейки для функций, которые вызывает тестируемый модуль */
|
||||
FAKE_VOID_FUNC(GPIO_PinInit, GPIO_Type *, uint32_t, const gpio_pin_config_t *);
|
||||
FAKE_VOID_FUNC(GPIO_PinWrite, GPIO_Type *, uint32_t, uint8_t);
|
||||
|
||||
/* 3. Тестируемый модуль — ПОСЛЕ фейков */
|
||||
#include "bsp/<module>.h"
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
RESET_FAKE(GPIO_PinInit);
|
||||
RESET_FAKE(GPIO_PinWrite);
|
||||
FFF_RESET_HISTORY();
|
||||
}
|
||||
|
||||
void tearDown(void) { }
|
||||
|
||||
void test_something(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_UINT8(0U, GPIO_PinWrite_fake.arg2_val);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_something);
|
||||
return UNITY_END();
|
||||
}
|
||||
```
|
||||
|
||||
Если тестируемому модулю не хватает stub-хедера (новый SDK-вызов) —
|
||||
добавить минимальные типы/сигнатуры в `tests/host/mocks/` (только то, что
|
||||
реально используется — не копировать весь SDK-хедер).
|
||||
|
||||
---
|
||||
|
||||
## Шаг 2 — Зарегистрировать в `tests/host/CMakeLists.txt`
|
||||
|
||||
```cmake
|
||||
# категория A — платформонезависимый, без MOCKS
|
||||
add_host_test(
|
||||
NAME test_<name>
|
||||
SOURCES <name>/test_<name>.c
|
||||
${PROJECT_SOURCE_DIR}/<путь-к-модулю>/<module>.c
|
||||
INCLUDES ${PROJECT_SOURCE_DIR}/<путь-к-инклюдам>
|
||||
)
|
||||
|
||||
# категория B — BSP-модуль, нужны MOCKS
|
||||
add_host_test(
|
||||
NAME test_<name>
|
||||
SOURCES <name>/test_<name>.c
|
||||
${PROJECT_SOURCE_DIR}/bsp/<name>/src/<name>.c
|
||||
INCLUDES ${PROJECT_SOURCE_DIR}/bsp/<name>/include
|
||||
${PROJECT_SOURCE_DIR}/bsp/common/include
|
||||
MOCKS ${BSP_MOCKS_DIR}
|
||||
)
|
||||
```
|
||||
|
||||
`add_host_test()` — вспомогательная CMake-функция, определённая в начале
|
||||
того же файла (`NAME`/`SOURCES`/`INCLUDES`/`MOCKS`). Каждый тест — свой
|
||||
исполняемый файл; `MOCKS` подключает `tests/host/mocks/` в include path
|
||||
**раньше** реального SDK, `INCLUDES` — явные пути, специфичные для теста
|
||||
(без скрытых глобальных путей). Если модуль использует `bsp_uart_host` через
|
||||
готовый мок — смотри пример `uart_host_mock_example` в том же файле.
|
||||
|
||||
Если тест компилируется с seam-макросом (как `test_runner.c` с
|
||||
`-DUNIT_TEST`, см. `firmware/test/README.md` §UNIT_TEST seam) — добавить:
|
||||
|
||||
```cmake
|
||||
target_compile_definitions(test_<name> PRIVATE UNIT_TEST)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FreeRTOS task view
|
||||
|
||||
Конфигурация `🐛 Debug: tft_app (FreeRTOS)` включает `"rtos": "FreeRTOS"` —
|
||||
cortex-debug разбирает структуры планировщика и показывает вкладку `RTOS`
|
||||
с таблицей задач: имя, состояние, использование стека, приоритет.
|
||||
|
||||
---
|
||||
|
||||
## Просмотр регистров периферии
|
||||
|
||||
Вкладка `Peripherals` показывает все блоки MIMXRT1052 по SVD-файлу
|
||||
`bsp/generated/startup/MIMXRT1052.xml`. Значения обновляются при каждой паузе.
|
||||
|
||||
---
|
||||
|
||||
## Ограничения
|
||||
|
||||
**MCU-Link монопольный ресурс.** `debug-server` и `flash-swd` не могут
|
||||
работать одновременно. Перед `flash-swd` остановите сервер (Ctrl+C).
|
||||
|
||||
**HIL-тесты vs отладка.** pyOCD также используется для HIL. Перед
|
||||
`just host::hil-run` остановите GDB-сервер.
|
||||
|
||||
**Power cycle после flash-swd обязателен.** VECTRESET не реинициализирует
|
||||
FlexSPI — только полное отключение питания гарантирует корректный cold-start.
|
||||
|
||||
**Только Debug-сборки.** Release компилируется с `-O2` без DWARF-символов.
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт (первый запуск)
|
||||
## Шаг 3 — Собрать и прогнать
|
||||
|
||||
```bash
|
||||
# 1. Убедиться что в .devcontainer/devcontainer.json есть (для Linux):
|
||||
# "runArgs": ["--add-host=host.docker.internal:host-gateway"]
|
||||
# конфигурация (один раз или после изменения CMakeLists)
|
||||
cmake --preset host-debug
|
||||
|
||||
# 2. Залить прошивку
|
||||
just host::flash-test-debug
|
||||
# сборка + тесты одной командой
|
||||
just build::test-host
|
||||
|
||||
# 3. Хост — запустить GDB-сервер
|
||||
just host::debug-server
|
||||
# конкретный тест с полным выводом Unity
|
||||
ctest --preset host-debug-test -R test_<name> -V
|
||||
|
||||
# 4. DevContainer — VSCode
|
||||
# Ctrl+Shift+D → 🐛 Debug: firmware_test → F5
|
||||
# напрямую — без обёртки CTest
|
||||
./build/host-debug/tests/host/test_<name>
|
||||
```
|
||||
|
||||
`just build::test-host` собирает под пресетом `host-debug` (`clang-17`,
|
||||
без ARM-специфики) и прогоняет весь набор через CTest. `host-release`
|
||||
собирает тот же набор с оптимизациями — используется в CI как
|
||||
дополнительный гейт.
|
||||
|
||||
---
|
||||
|
||||
## Чеклист
|
||||
|
||||
```bash
|
||||
[ ] tests/host/<name>/test_<name>.c — тест-файл (категория A или B)
|
||||
[ ] tests/host/mocks/*.h — новый stub-хедер, если модуль
|
||||
использует ранее не замоканный SDK-вызов
|
||||
[ ] tests/host/CMakeLists.txt — add_host_test(...) для нового теста
|
||||
[ ] just build::test-host — зелёная сборка + прогон
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Дерево файлов отладки
|
||||
## Справочник
|
||||
|
||||
```bash
|
||||
.
|
||||
├── .env # GDB_PORT, PYOCD_TARGET, PYOCD_FREQUENCY, FCB_PATH
|
||||
├── .vscode/
|
||||
│ ├── launch.json # cortex-debug конфигурации (3 проекта)
|
||||
│ └── tasks.json # preLaunchTask: build:*-debug
|
||||
├── bsp/generated/startup/
|
||||
│ └── MIMXRT1052.xml # SVD — регистры периферии
|
||||
├── just/
|
||||
│ └── host.just # debug-server, flash-swd-*
|
||||
└── tools/
|
||||
├── hil/ # uv-проект с pyocd
|
||||
└── host/
|
||||
├── flash_swd.py # FCB + HAB → Flash через pyOCD
|
||||
└── dcd/
|
||||
└── w25q128_fdcb.bin # FCB для W25Q128 Quad SPI
|
||||
```
|
||||
Полный API Unity (assertion-макросы), fff (создание фейков, `custom_fake`,
|
||||
проверка вызовов), работа со stub-хедерами и типичные ловушки (dangling
|
||||
pointer из `arg_history`, `static`-функции, `ScopeMismatch`-аналоги для
|
||||
host-тестов) — в [tests/host/README.md](../../../tests/host/README.md).
|
||||
|
|
|
|||
111
firmware/bootloader/CMakeLists.txt
Normal file
111
firmware/bootloader/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# firmware/bootloader/CMakeLists.txt Загрузчик — A/Б обновление tft_app через
|
||||
# microSD (Direct-XIP). Обновляется только через USB ROM + blhost / SWD.
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(
|
||||
bootloader
|
||||
VERSION 1.0.0
|
||||
LANGUAGES C ASM)
|
||||
|
||||
set(TARGET_NAME bootloader)
|
||||
|
||||
# Генерация version.h из шаблона
|
||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/version.h.in"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/generated/version.h" @ONLY)
|
||||
|
||||
# bootutil (MCUboot Direct-XIP) + TinyCrypt + ASN.1 — общий список с
|
||||
# host-тестами (tests/host/mcuboot_port/), см. firmware/bootloader/PLAN.md, Фаза
|
||||
# 2.
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port/bootutil_sources.cmake)
|
||||
|
||||
# bootloader_fatfs — bare-metal FatFS для чтения TFT_APP.BIN с SD (Фаза 3).
|
||||
add_subdirectory(fatfs)
|
||||
|
||||
add_executable(
|
||||
${TARGET_NAME}
|
||||
src/main.c
|
||||
src/cli.c
|
||||
src/protocol.c
|
||||
src/boot_select.c
|
||||
src/update_policy.c
|
||||
src/slot_version.c
|
||||
src/sd_update.c
|
||||
src/recovery.c
|
||||
src/led_status.c
|
||||
mcuboot_port/flash_map_backend.c
|
||||
mcuboot_port/keys.c
|
||||
${MCUBOOT_BOOTUTIL_SOURCES}
|
||||
${BSP_GENERATED}/clock_config.c
|
||||
${BSP_STARTUP_FILE}
|
||||
${BSP_SYSCALLS_FILE})
|
||||
|
||||
target_include_directories(${TARGET_NAME} PRIVATE src/
|
||||
${MCUBOOT_BOOTUTIL_INCLUDES})
|
||||
|
||||
target_include_directories(${TARGET_NAME}
|
||||
PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated")
|
||||
|
||||
target_compile_definitions(
|
||||
${TARGET_NAME} PRIVATE __STARTUP_INITIALIZE_RAMFUNCTION __STARTUP_CLEAR_BSS
|
||||
__STARTUP_INITIALIZE_NONCACHEDATA)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Зависимости. bsp_button — downgrade-override (удержание BSP_BUTTON_1).
|
||||
# bootloader_fatfs — чтение TFT_APP.BIN с SD (Фаза 3,
|
||||
# firmware/bootloader/fatfs/). bsp_sdram — smoke-test SDRAM/SEMC (Фаза 4,
|
||||
# bsp_sdram_configure()+_init()) — bootloader без DCD, сам поднимает SEMC на
|
||||
# время диагностики.
|
||||
# -----------------------------------------------------------------------------
|
||||
target_link_libraries(
|
||||
${TARGET_NAME}
|
||||
PRIVATE bsp_board
|
||||
bsp_led
|
||||
bsp_tick
|
||||
bsp_usb_cdc
|
||||
bsp_qspi_flash
|
||||
bsp_boot_xip_no_dcd
|
||||
bsp_button
|
||||
bsp_wdog
|
||||
bsp_boot_state
|
||||
bsp_sdram
|
||||
bootloader_fatfs)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# [DEV-ONLY] Диагностика Фазы 4 — CLI-команда "sdram_test" (dev_sdram_test.c):
|
||||
# 4-фазный тест SDRAM (адресная шина/шина данных/sequential/retention),
|
||||
# портирован из firmware_test/src/tests/test_sdram.c, прогоняется через
|
||||
# bsp_sdram_configure() вместо DCD. НЕ для production: только Debug —
|
||||
# Release/HAB-бинарь этот файл не содержит и о команде не знает.
|
||||
# -----------------------------------------------------------------------------
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
target_sources(${TARGET_NAME} PRIVATE src/dev_sdram_test.c)
|
||||
target_compile_definitions(${TARGET_NAME} PRIVATE BOOTLOADER_DEV_DIAGNOSTICS)
|
||||
endif()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Linker script — вариант flexspi_nor с m_text, ограниченным бюджетом bootloader
|
||||
# (256 KB, см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md). Без SDRAM (в отличие от
|
||||
# firmware_test) — bootloader SDRAM не использует.
|
||||
# -----------------------------------------------------------------------------
|
||||
target_link_options(
|
||||
${TARGET_NAME}
|
||||
PRIVATE
|
||||
-Wl,--gc-sections
|
||||
-Wl,--print-memory-usage
|
||||
-Wl,-Map=${CMAKE_BINARY_DIR}/bootloader.map
|
||||
-Wl,--defsym=__stack_size__=0x1000
|
||||
-Wl,--defsym=__heap_size__=0x1000
|
||||
-T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_bootloader_flexspi_nor.ld)
|
||||
|
||||
set_target_properties(${TARGET_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY
|
||||
${CMAKE_BINARY_DIR})
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Post-build: генерация .bin для прошивки через blhost
|
||||
# -----------------------------------------------------------------------------
|
||||
add_custom_command(
|
||||
TARGET ${TARGET_NAME}
|
||||
POST_BUILD
|
||||
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${TARGET_NAME}>
|
||||
${CMAKE_BINARY_DIR}/bootloader.bin
|
||||
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${TARGET_NAME}>
|
||||
COMMENT "Generating bootloader.bin")
|
||||
168
firmware/bootloader/README.md
Normal file
168
firmware/bootloader/README.md
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# bootloader
|
||||
|
||||
Загрузчик MIMXRT1052: выбирает и запускает приложение `tft_app` из одного из двух слотов
|
||||
(MCUboot, Direct-XIP), обновляет его с microSD, восстанавливает плату при зависании образа. Сам
|
||||
загрузчик прошивается только по USB ROM (blhost) или SWD — в поле не обновляется. Канал диагностики —
|
||||
USB CDC ACM (JSON-строки).
|
||||
|
||||
**Принцип работы** (карта памяти, выбор образа, обновление, версии, даунгрейд, recovery) —
|
||||
[BOOT_FLOW.md](../../docs/bootloader/BOOT_FLOW.md). LED-индикация — [LED_PATTERNS.md](../../docs/bootloader/LED_PATTERNS.md).
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### Сборка
|
||||
|
||||
```bash
|
||||
just build::build-bootloader-debug # bootloader.elf/.bin
|
||||
just build::hab-bootloader-debug # HAB-контейнер bootloader_hab.bin
|
||||
```
|
||||
|
||||
### Прошивка
|
||||
|
||||
SWD (для итеративной разработки, не требует смены boot-режима платы):
|
||||
|
||||
```bash
|
||||
just host::flash-swd-bootloader-debug
|
||||
# после прошивки обязателен power cycle платы
|
||||
```
|
||||
|
||||
USB ROM (SDP, плата в режиме Serial Downloader):
|
||||
|
||||
```bash
|
||||
just host::flash bootloader debug
|
||||
```
|
||||
|
||||
### Подключение
|
||||
|
||||
```bash
|
||||
screen /dev/cu.usbmodemXXXX # macOS; порт свой на каждое подключение
|
||||
```
|
||||
|
||||
```json
|
||||
→ {"type":"cmd","cmd":"ping"}
|
||||
← {"type":"pong"}
|
||||
|
||||
→ {"type":"cmd","cmd":"get_version"}
|
||||
← {"type":"version_response","fw":"0.1.0"}
|
||||
```
|
||||
|
||||
USB поднимается на каждой загрузке до обращения к SD, поэтому статусы видны, даже если подключиться
|
||||
заранее.
|
||||
|
||||
### Отладка
|
||||
|
||||
VSCode → `🐛 Debug: bootloader` — пересобирает, подключается к GDB-серверу
|
||||
(`just host::debug-server` должен быть запущен), останавливается на `main`. Под отладчиком аппаратный
|
||||
watchdog приостановлен, пошаговая отладка сбросами не сбивается.
|
||||
|
||||
---
|
||||
|
||||
## Архитектура
|
||||
|
||||
Загрузчик **не зависит от SDRAM** для своей работы (XIP только из W25Q, без DCD) и без дисплея/RTOS:
|
||||
инициализация, доступ к QSPI-flash, чтение FatFS с SD, проверка и выбор образа, прыжок. SEMC/SDRAM
|
||||
трогаются только диагностически (`bsp_sdram_configure()`, boot-time smoke-test) — реально их поднимает
|
||||
для себя уже само приложение в своём раннем startup.
|
||||
Линкер жёстко ограничивает код бюджетом области загрузчика (256 КБ) с `ASSERT` на границу Slot A —
|
||||
превышение становится ошибкой сборки, а не тихим заездом в чужую область.
|
||||
|
||||
```text
|
||||
firmware/bootloader/
|
||||
├── src/
|
||||
│ ├── main.c — точка входа: инициализация → одна попытка загрузки
|
||||
│ │ (SD-скан + recovery-гейт + прыжок) → цикл ожидания
|
||||
│ ├── boot_select.* — выбор валидного слота и прыжок в выбранный образ
|
||||
│ ├── slot_version.* — read-only проверка и чтение версии слота (без побочных
|
||||
│ │ эффектов на flash)
|
||||
│ ├── update_policy.* — чистая логика «ставить/пропустить» + целевой слот
|
||||
│ ├── recovery.* — чистая логика решения recovery (порог / фолбэк / режим)
|
||||
│ ├── sd_update.* — оркестрация: смонтировать SD, найти TFT_APP.BIN,
|
||||
│ │ установить в целевой слот с потоковой verify-записью
|
||||
│ ├── cli.* — построчный IO + диспетчеризация команд
|
||||
│ ├── protocol.* — сериализация исходящих событий
|
||||
│ ├── led_status.* — словарь LED-паттернов (см. ../../docs/bootloader/LED_PATTERNS.md)
|
||||
│ ├── dev_sdram_test.* — [DEV-ONLY, Debug] глубокий тест SDRAM по команде "sdram_test"
|
||||
│ └── version.h.in — шаблон версии (CMake → generated/version.h)
|
||||
│
|
||||
├── mcuboot_port/ — интеграция bootutil (MCUboot) поверх bsp_qspi_flash:
|
||||
│ flash_area_* на 2 слота, конфиг, публичный ключ ECDSA-P256
|
||||
├── fatfs/ — read-only FatFS для чтения TFT_APP.BIN с карты
|
||||
└── test_stub/ — самостоятельный подписанный образ-заглушка вместо
|
||||
tft_app для аппаратной проверки загрузчика
|
||||
```
|
||||
|
||||
`update_policy` и `recovery` — чистые функции без доступа к железу, целиком покрыты host-тестами.
|
||||
`boot_select`, `sd_update`, `flash_map_backend` — тонкий аппаратный слой поверх них.
|
||||
|
||||
---
|
||||
|
||||
## Протокол
|
||||
|
||||
USB CDC ACM, JSON-строки. Входящая строка — до `CLI_LINE_BUF_SIZE` (128) байт, исходящее сообщение —
|
||||
до `PROTO_BUF_SIZE` (192) байт (несимметрично: `qspi_info`/`sdram_test` длиннее старых сообщений).
|
||||
|
||||
**Команды хоста:**
|
||||
|
||||
| Команда | Ответ |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `{"type":"cmd","cmd":"ping"}` | `{"type":"pong"}` |
|
||||
| `{"type":"cmd","cmd":"get_version"}` | `{"type":"version_response","fw":"X.Y.Z"}` |
|
||||
| `{"type":"cmd","cmd":"wdog"}` | `{"type":"wdog","armed":…,"timeout_s":…,"recovered":…,"reset_count":…,"threshold":…}` |
|
||||
| `{"type":"cmd","cmd":"smoke_status"}` | `{"type":"status","state":"smoke_pass"}` / `"smoke_fail"` — результат boot-time smoke-теста SDRAM/SEMC (переспрос, см. ниже) |
|
||||
| `{"type":"cmd","cmd":"qspi_info"}` | `{"type":"qspi_info","chip":"W25Q128","mfr":"0xEF","cap_byte":"0x18","size_mb":16,"pass":true}` (переспрос, см. ниже) |
|
||||
| `{"type":"cmd","cmd":"sdram_test"}` <br> **[DEV-ONLY, Debug-сборка]** | серия из 6 `{"type":"sdram_test","phase":"…","pass":…,"duration_ms":…,"fail_addr":"…","expected":"…","got":"…"}` (`configure`/`address_bus`/`data_bus`/`sequential`/`retention`/`summary`) — блокирует главный цикл на ~4 с. Нет в Release/HAB (`BOOTLOADER_DEV_DIAGNOSTICS`) |
|
||||
|
||||
`smoke_status`/`qspi_info` ничего не отвечают, если соответствующий boot-time чек ещё не отработал —
|
||||
в штатной последовательности `main.c` такого не бывает.
|
||||
|
||||
**Исходящие статусы** `{"type":"status","state":"…"}`:
|
||||
|
||||
| Состояние | Когда |
|
||||
| ---------------- | ------------------------------------------ |
|
||||
| `waiting_for_sd` | нет валидного слота, ждём карту |
|
||||
| `installing` | идёт запись образа в слот |
|
||||
| `update_skipped` | кандидат отклонён по версии |
|
||||
| `recovery_mode` | плата в режиме восстановления |
|
||||
| `smoke_pass` | boot-time smoke-тест SDRAM/SEMC прошёл |
|
||||
| `smoke_fail` | boot-time smoke-тест SDRAM/SEMC провалился |
|
||||
|
||||
**Ошибки** `{"ok":false,"error":"…"}`: `SD_CANDIDATE_INVALID`, `SD_INSTALL_WRITE_FAILED`,
|
||||
`SD_INSTALL_REJECTED`, `SD_DOWNGRADE_ERASE_FAILED`, `PARSE_ERR`, `UNKNOWN_CMD`, `LINE_TOO_LONG`.
|
||||
|
||||
**Автоматически на старте, без команды:** `wdog` — если предыдущий сброс был по watchdog
|
||||
(`recovered:true`); `qspi_info` и `smoke_pass`/`smoke_fail` — сразу после соответствующей проверки.
|
||||
Все три — best-effort: хост почти никогда не успевает открыть порт к этому моменту (USB enumeration),
|
||||
поэтому у `qspi_info`/`smoke_status` (но не у одноразового boot-time `wdog`) есть команда-переспрос
|
||||
в таблице выше.
|
||||
|
||||
LED-индикация, соответствующая этим состояниям, — [LED_PATTERNS.md](../../docs/bootloader/LED_PATTERNS.md).
|
||||
|
||||
---
|
||||
|
||||
## Тесты
|
||||
|
||||
**Host** — вся логика без железа (выбор слота, сравнение версий, политика установки, решение
|
||||
recovery, протокол, CLI) на in-memory flash-фейках:
|
||||
|
||||
```bash
|
||||
just build::test-host
|
||||
```
|
||||
|
||||
**Аппаратный стенд** — сборка и подпись образов-заглушек, замещающих `tft_app` при ручной проверке
|
||||
на плате (здоровые образы + варианты с зависанием на разных стадиях для проверки восстановления):
|
||||
|
||||
```bash
|
||||
just build::build-mcuboot-stub
|
||||
```
|
||||
|
||||
Чек-листы ручной проверки лежат рядом со стендом в `test_stub/`.
|
||||
|
||||
---
|
||||
|
||||
## Версионирование
|
||||
|
||||
Версия задаётся `project(bootloader VERSION X.Y.Z)` в `CMakeLists.txt` и прокидывается через
|
||||
`configure_file(src/version.h.in → generated/version.h)` в строку, которую возвращает
|
||||
`get_version`. `version.h` генерируется, вручную не редактируется.
|
||||
35
firmware/bootloader/fatfs/CMakeLists.txt
Normal file
35
firmware/bootloader/fatfs/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# bootloader_fatfs — FatFS, скомпилированный с bare-metal ffconf.h для
|
||||
# bootloader.
|
||||
#
|
||||
# ff.c, fsl_sd_disk.c, diskio_sd.c включают ff.h → ffconf.h. Все три
|
||||
# компилируются здесь, чтобы видели один и тот же ffconf.h из
|
||||
# ${CMAKE_CURRENT_SOURCE_DIR}/include.
|
||||
#
|
||||
# Не шарить с firmware_test_fatfs: bootloader и firmware_test —
|
||||
# взаимоисключающие прошивки одной платы (см. firmware/bootloader/PLAN.md,
|
||||
# Фаза 3) — зависимость от таргета с именем "firmware_test" была бы неверной
|
||||
# связью. tft_app заведёт свой аналогичный таргет с FreeRTOS ffconf.h.
|
||||
#
|
||||
# Отличие от firmware_test_fatfs: FF_FS_READONLY=1 — bootloader только читает
|
||||
# TFT_APP.BIN, никогда не пишет на SD (см. include/ffconf.h).
|
||||
|
||||
add_library(
|
||||
bootloader_fatfs STATIC
|
||||
${SDK_FATFS_FF_SRC} # sdk/middleware/fatfs/source/ff.c
|
||||
${SDK_FATFS_SD_DISK_SRC} # sdk/middleware/fatfs/source/fsl_sd_disk/fsl_sd_disk.c
|
||||
${PORT_FATFS_SD_SRC} # port/fatfs/sd/src/diskio_sd.c
|
||||
src/diskio.c)
|
||||
|
||||
# include/ первым — ffconf.h отсюда должен перекрыть любой шаблонный
|
||||
target_include_directories(
|
||||
bootloader_fatfs
|
||||
PUBLIC include # ffconf.h, виден потребителям (sd_update.c)
|
||||
PRIVATE src)
|
||||
|
||||
target_link_libraries(
|
||||
bootloader_fatfs
|
||||
PUBLIC port_fatfs_sd # diskio_sd.h, ff.h, fsl_sd_disk.h, bsp_sd
|
||||
PRIVATE bsp_sdmmc_config) # SD_ENABLED транзитивно из bsp_sd, но явно для
|
||||
# ясности
|
||||
|
||||
target_compile_options(bootloader_fatfs PRIVATE -w)
|
||||
86
firmware/bootloader/fatfs/include/ffconf.h
Normal file
86
firmware/bootloader/fatfs/include/ffconf.h
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* ffconf.h — конфигурация FatFS для bootloader (bare-metal, только SD).
|
||||
*
|
||||
* Копия паттерна firmware/test/fatfs/include/ffconf.h — не шарить: bootloader
|
||||
* и firmware_test взаимоисключающие прошивки одной платы (см.
|
||||
* firmware/bootloader/PLAN.md, Фаза 3). tft_app заведёт свой аналогичный
|
||||
* таргет с FreeRTOS ffconf.h.
|
||||
*
|
||||
* Ключевое отличие от firmware_test:
|
||||
* FF_FS_READONLY = 1 — bootloader только читает TFT_APP.BIN с SD, никогда
|
||||
* не пишет на карту; убирает f_write и весь путь
|
||||
* записи FatFS из сборки.
|
||||
* FF_FS_REENTRANT = 0 — нет RTOS, нет мьютексов
|
||||
* FF_VOLUMES = 3 — 0: зарезервирован, 1: зарезервирован, 2: SD
|
||||
* FF_MAX_SS = 512 — SD всегда 512 байт/сектор, ioctl не нужен
|
||||
*/
|
||||
|
||||
#ifndef _FFCONF_H_
|
||||
#define _FFCONF_H_
|
||||
|
||||
#define FFCONF_DEF 80286
|
||||
|
||||
/*---------------------------------------------------------------------------/
|
||||
/ MSDK adaptation
|
||||
/---------------------------------------------------------------------------*/
|
||||
#define SD_DISK_ENABLE 1
|
||||
|
||||
/*---------------------------------------------------------------------------/
|
||||
/ Function Configurations
|
||||
/---------------------------------------------------------------------------*/
|
||||
#define FF_FS_READONLY 1 /* bootloader никогда не пишет на SD */
|
||||
#define FF_FS_MINIMIZE 0
|
||||
#define FF_USE_FIND 0
|
||||
#define FF_USE_MKFS 0 /* f_mkfs не нужна — карта уже отформатирована */
|
||||
#define FF_USE_FASTSEEK 0
|
||||
#define FF_USE_EXPAND 0
|
||||
#define FF_USE_CHMOD 0
|
||||
#define FF_USE_LABEL 0
|
||||
#define FF_USE_FORWARD 0
|
||||
#define FF_USE_STRFUNC 0
|
||||
#define FF_PRINT_LLI 0
|
||||
#define FF_PRINT_FLOAT 0
|
||||
#define FF_STRF_ENCODE 3
|
||||
|
||||
/*---------------------------------------------------------------------------/
|
||||
/ Locale
|
||||
/---------------------------------------------------------------------------*/
|
||||
#define FF_CODE_PAGE 437 /* U.S. — минимальный, имена файлов ASCII */
|
||||
|
||||
#define FF_USE_LFN 0 /* только 8.3 — достаточно для TFT_APP.BIN */
|
||||
#define FF_MAX_LFN 255
|
||||
#define FF_LFN_UNICODE 0
|
||||
#define FF_LFN_BUF 255
|
||||
#define FF_SFN_BUF 12
|
||||
#define FF_FS_RPATH 0 /* относительные пути не нужны */
|
||||
|
||||
/*---------------------------------------------------------------------------/
|
||||
/ Drive/Volume Configurations
|
||||
/---------------------------------------------------------------------------*/
|
||||
#define FF_VOLUMES 3 /* 0: зарезервирован, 1: зарезервирован, 2: SD */
|
||||
#define FF_STR_VOLUME_ID 0
|
||||
#define FF_MULTI_PARTITION 0
|
||||
|
||||
#define FF_MIN_SS 512
|
||||
#define FF_MAX_SS 512 /* SD: всегда 512, GET_SECTOR_SIZE не нужен */
|
||||
|
||||
#define FF_LBA64 0
|
||||
#define FF_MIN_GPT 0x10000000
|
||||
#define FF_USE_TRIM 0
|
||||
|
||||
/*---------------------------------------------------------------------------/
|
||||
/ System Configurations
|
||||
/---------------------------------------------------------------------------*/
|
||||
#define FF_FS_TINY 0
|
||||
#define FF_FS_EXFAT 0 /* exFAT требует LFN — оба отключены */
|
||||
#define FF_FS_NORTC 1
|
||||
#define FF_NORTC_MON 1
|
||||
#define FF_NORTC_MDAY 1
|
||||
#define FF_NORTC_YEAR 2024
|
||||
#define FF_FS_NOFSINFO 0
|
||||
#define FF_FS_LOCK 0
|
||||
|
||||
#define FF_FS_REENTRANT 0 /* bare-metal: нет RTOS, нет мьютексов */
|
||||
/* FF_FS_TIMEOUT и FF_SYNC_t не нужны */
|
||||
|
||||
#endif /* _FFCONF_H_ */
|
||||
53
firmware/bootloader/fatfs/src/diskio.c
Normal file
53
firmware/bootloader/fatfs/src/diskio.c
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* diskio.c — FatFS diskio диспетчер для bootloader.
|
||||
*
|
||||
* FF_VOLUMES=3: диски 0 и 1 — заглушки, диск 2 = SDDISK (microSD).
|
||||
* W25Q и RAM-диск отсутствуют — нет зависимости на bsp_qspi_flash (слоты A/Б
|
||||
* читаются/пишутся напрямую через flash_area_*, в обход FatFS).
|
||||
*/
|
||||
|
||||
#include "diskio.h"
|
||||
|
||||
#include "port/fatfs/diskio_sd.h"
|
||||
|
||||
#define SDDISK 2U
|
||||
|
||||
DSTATUS disk_initialize(BYTE pdrv)
|
||||
{
|
||||
if (pdrv == SDDISK)
|
||||
{
|
||||
return microsd_disk_initialize(pdrv);
|
||||
}
|
||||
|
||||
return STA_NOINIT;
|
||||
}
|
||||
|
||||
DSTATUS disk_status(BYTE pdrv)
|
||||
{
|
||||
if (pdrv == SDDISK)
|
||||
{
|
||||
return microsd_disk_status(pdrv);
|
||||
}
|
||||
|
||||
return STA_NOINIT;
|
||||
}
|
||||
|
||||
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count)
|
||||
{
|
||||
if (pdrv == SDDISK)
|
||||
{
|
||||
return microsd_disk_read(pdrv, buff, sector, count);
|
||||
}
|
||||
|
||||
return RES_PARERR;
|
||||
}
|
||||
|
||||
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff)
|
||||
{
|
||||
if (pdrv == SDDISK)
|
||||
{
|
||||
return microsd_disk_ioctl(pdrv, cmd, buff);
|
||||
}
|
||||
|
||||
return RES_PARERR;
|
||||
}
|
||||
43
firmware/bootloader/mcuboot_port/bootutil_sources.cmake
Normal file
43
firmware/bootloader/mcuboot_port/bootutil_sources.cmake
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# mcuboot_port/bootutil_sources.cmake
|
||||
#
|
||||
# Общий список файлов bootutil (MCUboot) + TinyCrypt + ASN.1-парсер,
|
||||
# используемых и реальным ARM-таргетом (firmware/bootloader), и host-тестами
|
||||
# (tests/host/mcuboot_port/)
|
||||
|
||||
set(MCUBOOT_OPENSOURCE_DIR
|
||||
${CMAKE_SOURCE_DIR}/sdk/middleware/mcuboot_opensource)
|
||||
set(MCUBOOT_BOOTUTIL_DIR ${MCUBOOT_OPENSOURCE_DIR}/boot/bootutil)
|
||||
set(MCUBOOT_EXT_DIR ${MCUBOOT_OPENSOURCE_DIR}/ext)
|
||||
|
||||
set(MCUBOOT_BOOTUTIL_SOURCES
|
||||
${MCUBOOT_BOOTUTIL_DIR}/src/loader.c
|
||||
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_misc.c
|
||||
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c
|
||||
${MCUBOOT_BOOTUTIL_DIR}/src/tlv.c
|
||||
${MCUBOOT_BOOTUTIL_DIR}/src/image_validate.c
|
||||
${MCUBOOT_BOOTUTIL_DIR}/src/image_ecdsa.c
|
||||
${MCUBOOT_BOOTUTIL_DIR}/src/fault_injection_hardening.c
|
||||
${MCUBOOT_BOOTUTIL_DIR}/src/swap_scratch.c
|
||||
${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/ecc.c
|
||||
${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/ecc_dsa.c
|
||||
${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/sha256.c
|
||||
${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/utils.c
|
||||
${MCUBOOT_EXT_DIR}/mbedtls-asn1/src/asn1parse.c
|
||||
${MCUBOOT_EXT_DIR}/mbedtls-asn1/src/platform_util.c)
|
||||
|
||||
set(MCUBOOT_BOOTUTIL_INCLUDES
|
||||
${MCUBOOT_BOOTUTIL_DIR}/include
|
||||
${MCUBOOT_BOOTUTIL_DIR}/src
|
||||
${MCUBOOT_EXT_DIR}/tinycrypt/lib/include
|
||||
${MCUBOOT_EXT_DIR}/mbedtls-asn1/include
|
||||
${CMAKE_CURRENT_LIST_DIR} # sysflash.h, mcuboot_config.h, flash_map.h,
|
||||
# flash_map_backend.h
|
||||
)
|
||||
|
||||
set(MCUBOOT_VENDORED_COMPILE_OPTIONS -w)
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "Clang")
|
||||
list(APPEND MCUBOOT_VENDORED_COMPILE_OPTIONS -fno-sanitize=address,undefined)
|
||||
endif()
|
||||
set_source_files_properties(
|
||||
${MCUBOOT_BOOTUTIL_SOURCES} PROPERTIES COMPILE_OPTIONS
|
||||
"${MCUBOOT_VENDORED_COMPILE_OPTIONS}")
|
||||
62
firmware/bootloader/mcuboot_port/flash_map.h
Normal file
62
firmware/bootloader/mcuboot_port/flash_map.h
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* @file flash_map.h
|
||||
* @brief Контракт bootutil на "область флеша" — реализуется
|
||||
* flash_map_backend.c (реальный, над bsp_qspi_flash) или
|
||||
* fake_flash_map_backend.c (host-тесты, in-memory буфер).
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef FLASH_MAP_H_
|
||||
#define FLASH_MAP_H_
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @brief Область на flash-устройстве.
|
||||
*
|
||||
* Несколько устройств в системе не предполагается (см. FLASH_DEVICE_ID в
|
||||
* sysflash.h) — fa_device_id всегда равен FLASH_DEVICE_ID.
|
||||
*/
|
||||
struct flash_area
|
||||
{
|
||||
uint8_t fa_id; /**< ID области, уникален в системе. */
|
||||
uint8_t fa_device_id; /**< ID flash-устройства. */
|
||||
uint16_t pad16;
|
||||
uint32_t fa_off; /**< Смещение области от начала устройства. */
|
||||
uint32_t fa_size; /**< Размер области, байт. */
|
||||
};
|
||||
|
||||
/** @brief Сектор внутри области (смещение относительно начала области). */
|
||||
struct flash_sector
|
||||
{
|
||||
uint32_t fs_off;
|
||||
uint32_t fs_size;
|
||||
};
|
||||
|
||||
/** @brief Базовый адрес flash-устройства в адресном пространстве MCU (XIP). */
|
||||
int flash_device_base(uint8_t fd_id, uintptr_t *ret);
|
||||
|
||||
int flash_area_open(uint8_t id, const struct flash_area **area);
|
||||
void flash_area_close(const struct flash_area *area);
|
||||
|
||||
/* Read/write/erase — смещение относительно начала области. */
|
||||
int flash_area_read(const struct flash_area *area, uint32_t off, void *dst, uint32_t len);
|
||||
int flash_area_write(const struct flash_area *area, uint32_t off, const void *src, uint32_t len);
|
||||
int flash_area_erase(const struct flash_area *area, uint32_t off, uint32_t len);
|
||||
|
||||
/** @brief Минимальное выравнивание записи. */
|
||||
uint8_t flash_area_align(const struct flash_area *area);
|
||||
|
||||
/** @brief Значение стёртого байта (0xFF для NOR). */
|
||||
uint8_t flash_area_erased_val(const struct flash_area *area);
|
||||
|
||||
/** @brief Прочитать len байт с off и проверить что это стёртая область. */
|
||||
int flash_area_read_is_empty(const struct flash_area *area, uint32_t off, void *dst, uint32_t len);
|
||||
|
||||
int flash_area_get_sectors(int fa_id, uint32_t *count, struct flash_sector *sectors);
|
||||
int flash_area_get_sector(const struct flash_area *fa, uint32_t off, struct flash_sector *sector);
|
||||
|
||||
int flash_area_id_from_image_slot(int slot);
|
||||
int flash_area_id_to_multi_image_slot(int image_index, int area_id);
|
||||
|
||||
#endif /* FLASH_MAP_H_ */
|
||||
280
firmware/bootloader/mcuboot_port/flash_map_backend.c
Normal file
280
firmware/bootloader/mcuboot_port/flash_map_backend.c
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
/**
|
||||
* @file flash_map_backend.c
|
||||
* @brief Реализация flash_map.h поверх bsp_qspi_flash — Slot A/Б из
|
||||
* docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md.
|
||||
*
|
||||
* bsp_qspi_read/write_page/erase_sector() принимают flash-relative адрес
|
||||
* (0-based от начала чипа, IPCR0 FlexSPI IP-команд) — НЕ XIP-адрес
|
||||
* (0x60000000+). fa_off здесь — то же самое flash-relative смещение.
|
||||
* flash_device_base() — единственное место, где встречается XIP-адрес
|
||||
* 0x60000000: он нужен boot_select.c для вычисления адреса прыжка
|
||||
* (flash_base + fa_off + hdr_size), но не самим read/write/erase.
|
||||
*
|
||||
* @pre bsp_qspi_init() должен быть вызван до любой flash_area_* функции
|
||||
* (main.c, до boot_go()).
|
||||
*/
|
||||
|
||||
#include "bsp/qspi_flash.h"
|
||||
#include "bsp/wdog.h"
|
||||
#include "flash_map.h"
|
||||
#include "led_status.h"
|
||||
#include "sysflash/sysflash.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#define ERASED_VAL 0xFFU
|
||||
|
||||
/* Slot A/Б — см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md. Смещения —
|
||||
* flash-relative (от начала чипа), не XIP-адрес. */
|
||||
static const struct flash_area g_s_areas[2] = {
|
||||
{ .fa_id = 0U,
|
||||
.fa_device_id = FLASH_DEVICE_ID,
|
||||
.pad16 = 0U,
|
||||
.fa_off = 0x00040000UL,
|
||||
.fa_size = 0x00200000UL }, /* Slot A: 0x60040000, 2 МБ */
|
||||
{ .fa_id = 1U,
|
||||
.fa_device_id = FLASH_DEVICE_ID,
|
||||
.pad16 = 0U,
|
||||
.fa_off = 0x00240000UL,
|
||||
.fa_size = 0x00200000UL }, /* Slot Б: 0x60240000, 2 МБ */
|
||||
};
|
||||
|
||||
/* ── Постраничная запись (аналог NXP flash_area_write_internal) ─────────
|
||||
*
|
||||
* bsp_qspi_write_page() пишет ровно BSP_QSPI_PAGE_SIZE (256) байт по
|
||||
* странично-выровненному адресу. Запись 0xFF поверх уже запрограммированных
|
||||
* байт — не изменяет их (NOR program может только сбрасывать биты 1→0,
|
||||
* запись 0xFF не запрашивает сброс ни одного бита) — поэтому безопасно
|
||||
* "перезатирать" уже записанную часть страницы буфером, где нетронутая
|
||||
* часть заполнена ERASED_VAL: bootutil пишет монотонно возрастающими
|
||||
* смещениями, повторно данные не перезаписывает.
|
||||
*/
|
||||
static int write_page_chunked(uint32_t dst_addr, const uint8_t *p_src, uint32_t len)
|
||||
{
|
||||
uint8_t page_buf[BSP_QSPI_PAGE_SIZE];
|
||||
|
||||
uint32_t chunk_ofs = dst_addr % BSP_QSPI_PAGE_SIZE;
|
||||
uint32_t page_addr = dst_addr - chunk_ofs;
|
||||
uint32_t chunk_size = BSP_QSPI_PAGE_SIZE - chunk_ofs;
|
||||
|
||||
while (len > 0U)
|
||||
{
|
||||
if (chunk_size > len)
|
||||
{
|
||||
chunk_size = len;
|
||||
}
|
||||
|
||||
memset(page_buf, ERASED_VAL, BSP_QSPI_PAGE_SIZE);
|
||||
memcpy(page_buf + chunk_ofs, p_src, chunk_size);
|
||||
|
||||
if (bsp_qspi_write_page(page_addr, page_buf) != BSP_OK)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
p_src += chunk_size;
|
||||
len -= chunk_size;
|
||||
chunk_ofs = 0U;
|
||||
chunk_size = BSP_QSPI_PAGE_SIZE;
|
||||
page_addr += BSP_QSPI_PAGE_SIZE;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── flash_map.h contract ─────────────────────────────────────────────── */
|
||||
|
||||
int flash_device_base(uint8_t fd_id, uintptr_t *ret)
|
||||
{
|
||||
if (fd_id != FLASH_DEVICE_ID)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
*ret = 0x60000000UL; /* XIP-mapped база — только для вычисления адреса прыжка */
|
||||
return 0;
|
||||
}
|
||||
|
||||
int flash_area_open(uint8_t id, const struct flash_area **area)
|
||||
{
|
||||
if (id >= 2U)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
*area = &g_s_areas[id];
|
||||
return 0;
|
||||
}
|
||||
|
||||
void flash_area_close(const struct flash_area *area)
|
||||
{
|
||||
(void) area;
|
||||
}
|
||||
|
||||
int flash_area_read(const struct flash_area *area, uint32_t off, void *dst, uint32_t len)
|
||||
{
|
||||
if (off + len > area->fa_size)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
return (bsp_qspi_read(area->fa_off + off, (uint8_t *) dst, len) == BSP_OK) ? 0 : -1;
|
||||
}
|
||||
|
||||
int flash_area_write(const struct flash_area *area, uint32_t off, const void *src, uint32_t len)
|
||||
{
|
||||
if (off + len > area->fa_size)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
return write_page_chunked(area->fa_off + off, (const uint8_t *) src, len);
|
||||
}
|
||||
|
||||
int flash_area_erase(const struct flash_area *area, uint32_t off, uint32_t len)
|
||||
{
|
||||
if ((off + len > area->fa_size) || ((off % BSP_QSPI_SECTOR_SIZE) != 0U) ||
|
||||
((len % BSP_QSPI_SECTOR_SIZE) != 0U))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Fast-path: стирание всей области целиком (off=0, len=fa_size), кратно
|
||||
* 64 КБ — блочное стирание ~5x быстрее посекторного (2 МБ: ~4.8 с против
|
||||
* ~23 с, см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md, "Обоснование
|
||||
* размеров"). Ускоряет и sd_update (Фаза 3), и штатный revert-erase
|
||||
* bootutil (boot_select_or_erase() в loader.c) — они уже зовут
|
||||
* flash_area_erase(fap, 0, flash_area_get_size(fap)) без изменений.
|
||||
* Частичное/невыровненное стирание (напр. один трейлер) — прежний
|
||||
* посекторный путь ниже. */
|
||||
if ((off == 0U) && (len == area->fa_size) && ((len % BSP_QSPI_BLOCK_64K_SIZE) == 0U))
|
||||
{
|
||||
uint32_t block_addr = area->fa_off;
|
||||
for (; len > 0U; len -= BSP_QSPI_BLOCK_64K_SIZE)
|
||||
{
|
||||
/* Кормим watchdog поблочно: стирание 2 МБ ~4.8 c — это реальный
|
||||
* прогресс, но один блочный вызов не должен упереться в таймаут.
|
||||
* Зависание самого стирания флеша всё равно ловится: refresh — по
|
||||
* ЗАВЕРШЕНИИ блока, а не перед ним. */
|
||||
bsp_wdog_refresh();
|
||||
/* Прогресс-хук индикации — no-op вне окна установки, ничего не
|
||||
* рисует на revert/recovery-стирании (та же функция). Вызывается
|
||||
* исправно каждый блок, но САМ блок (~150 мс) идёт под
|
||||
* qspi_irq_lock() — глушит SysTick, от которого тикает
|
||||
* bsp_tick_get_ms(); на глаз это видно как подвисание/дёрганое
|
||||
* мигание, не гладкие 250/250 (известное ограничение, см. @note
|
||||
* у led_status_tick_install() в led_status.h). */
|
||||
led_status_tick_install();
|
||||
if (bsp_qspi_erase_block_64k(block_addr) != BSP_OK)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
block_addr += BSP_QSPI_BLOCK_64K_SIZE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t addr = area->fa_off + off;
|
||||
for (; len > 0U; len -= BSP_QSPI_SECTOR_SIZE)
|
||||
{
|
||||
bsp_wdog_refresh(); /* см. выше — посекторный путь тоже длинный */
|
||||
if (bsp_qspi_erase_sector(addr) != BSP_OK)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
addr += BSP_QSPI_SECTOR_SIZE;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t flash_area_align(const struct flash_area *area)
|
||||
{
|
||||
(void) area;
|
||||
return 1U;
|
||||
}
|
||||
|
||||
uint8_t flash_area_erased_val(const struct flash_area *area)
|
||||
{
|
||||
(void) area;
|
||||
return ERASED_VAL;
|
||||
}
|
||||
|
||||
int flash_area_read_is_empty(const struct flash_area *area, uint32_t off, void *dst, uint32_t len)
|
||||
{
|
||||
if (flash_area_read(area, off, dst, len) != 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
const uint8_t *p_buf = (const uint8_t *) dst;
|
||||
for (uint32_t i = 0U; i < len; i++)
|
||||
{
|
||||
if (p_buf[i] != ERASED_VAL)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int flash_area_get_sector(const struct flash_area *fa, uint32_t off, struct flash_sector *sector)
|
||||
{
|
||||
if (off >= fa->fa_size)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
sector->fs_off = (off / BSP_QSPI_SECTOR_SIZE) * BSP_QSPI_SECTOR_SIZE;
|
||||
sector->fs_size = BSP_QSPI_SECTOR_SIZE;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int flash_area_get_sectors(int fa_id, uint32_t *count, struct flash_sector *sectors)
|
||||
{
|
||||
const struct flash_area *fa;
|
||||
uint32_t max_cnt = *count;
|
||||
|
||||
if (flash_area_open((uint8_t) fa_id, &fa) != 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint32_t rem_len = fa->fa_size;
|
||||
*count = 0U;
|
||||
while ((rem_len > 0U) && (*count < max_cnt))
|
||||
{
|
||||
sectors[*count].fs_off = BSP_QSPI_SECTOR_SIZE * (*count);
|
||||
sectors[*count].fs_size = BSP_QSPI_SECTOR_SIZE;
|
||||
(*count)++;
|
||||
rem_len -= BSP_QSPI_SECTOR_SIZE;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int flash_area_id_from_multi_image_slot(int image_index, int slot)
|
||||
{
|
||||
switch (slot)
|
||||
{
|
||||
case 0:
|
||||
return FLASH_AREA_IMAGE_PRIMARY(image_index);
|
||||
case 1:
|
||||
return FLASH_AREA_IMAGE_SECONDARY(image_index);
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int flash_area_id_from_image_slot(int slot)
|
||||
{
|
||||
return flash_area_id_from_multi_image_slot(0, slot);
|
||||
}
|
||||
|
||||
int flash_area_id_to_multi_image_slot(int image_index, int area_id)
|
||||
{
|
||||
if (area_id == FLASH_AREA_IMAGE_PRIMARY(image_index))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (area_id == FLASH_AREA_IMAGE_SECONDARY(image_index))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* @file flash_map_backend.h
|
||||
* @brief Инлайн-аксессоры flash_area/flash_sector, требуемые bootutil.
|
||||
*/
|
||||
|
||||
#ifndef FLASH_MAP_BACKEND_H_
|
||||
#define FLASH_MAP_BACKEND_H_
|
||||
|
||||
#include "flash_map.h"
|
||||
|
||||
static inline uint8_t flash_area_get_id(const struct flash_area *fa)
|
||||
{
|
||||
return fa->fa_id;
|
||||
}
|
||||
|
||||
static inline uint8_t flash_area_get_device_id(const struct flash_area *fa)
|
||||
{
|
||||
return fa->fa_device_id;
|
||||
}
|
||||
|
||||
static inline uint32_t flash_area_get_off(const struct flash_area *fa)
|
||||
{
|
||||
return fa->fa_off;
|
||||
}
|
||||
|
||||
static inline uint32_t flash_area_get_size(const struct flash_area *fa)
|
||||
{
|
||||
return fa->fa_size;
|
||||
}
|
||||
|
||||
static inline uint32_t flash_sector_get_off(const struct flash_sector *fs)
|
||||
{
|
||||
return fs->fs_off;
|
||||
}
|
||||
|
||||
static inline uint32_t flash_sector_get_size(const struct flash_sector *fs)
|
||||
{
|
||||
return fs->fs_size;
|
||||
}
|
||||
|
||||
#endif /* FLASH_MAP_BACKEND_H_ */
|
||||
27
firmware/bootloader/mcuboot_port/keys.c
Normal file
27
firmware/bootloader/mcuboot_port/keys.c
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* @file keys.c
|
||||
* @brief Таблица публичных ключей bootutil.
|
||||
*
|
||||
* MCUBOOT_HW_KEY / MCUBOOT_BUILTIN_KEY не определены (см. mcuboot_config.h)
|
||||
* — bootutil использует стандартный путь: TLV образа несёт хэш ключа,
|
||||
* bootutil ищет совпадение в bootutil_keys[] и проверяет подпись найденным
|
||||
* ключом. По образцу sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/keys.c.
|
||||
*/
|
||||
|
||||
#include <bootutil/sign_key.h>
|
||||
#include <mcuboot_config/mcuboot_config.h>
|
||||
|
||||
#if defined(MCUBOOT_SIGN_EC256)
|
||||
#include "keys/bootloader_test_ecdsa_pub.c"
|
||||
#else
|
||||
#error "No public key available for given signing algorithm."
|
||||
#endif
|
||||
|
||||
const struct bootutil_key bootutil_keys[] = {
|
||||
{
|
||||
.key = ecdsa_pub_key,
|
||||
.len = &ecdsa_pub_key_len,
|
||||
},
|
||||
};
|
||||
|
||||
const int bootutil_key_cnt = 1;
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* @file bootloader_test_ecdsa_pub.c
|
||||
* @brief Публичный ключ ECDSA P-256 — сгенерирован из тестового
|
||||
* sample-ключа MCUboot (sdk/middleware/mcuboot_opensource/root-ec-p256.pem)
|
||||
* командой `imgtool.py getpub --lang c`.
|
||||
*
|
||||
* ВНИМАНИЕ: это публичный, широко известный sample-ключ проекта MCUboot,
|
||||
* НЕ производственный секрет. Используется для Фазы 2 (host-тесты + первая
|
||||
* проверка на железе). Перед серийным производством должен быть заменён
|
||||
* на реальный production-ключ (приватная часть — вне репозитория, см.
|
||||
* жизненный цикл ключей HAB в docs/mimxrt1052/HAB_GUIDE.md — аналогичная
|
||||
* процедура нужна для ключа подписи tft_app-образов).
|
||||
*/
|
||||
|
||||
/* Autogenerated by imgtool.py, do not edit. */
|
||||
const unsigned char ecdsa_pub_key[] = {
|
||||
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06,
|
||||
0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x2a,
|
||||
0xcb, 0x40, 0x3c, 0xe8, 0xfe, 0xed, 0x5b, 0xa4, 0x49, 0x95, 0xa1, 0xa9, 0x1d, 0xae,
|
||||
0xe8, 0xdb, 0xbe, 0x19, 0x37, 0xcd, 0x14, 0xfb, 0x2f, 0x24, 0x57, 0x37, 0xe5, 0x95,
|
||||
0x39, 0x88, 0xd9, 0x94, 0xb9, 0xd6, 0x5a, 0xeb, 0xd7, 0xcd, 0xd5, 0x30, 0x8a, 0xd6,
|
||||
0xfe, 0x48, 0xb2, 0x4a, 0x6a, 0x81, 0x0e, 0xe5, 0xf0, 0x7d, 0x8b, 0x68, 0x34, 0xcc,
|
||||
0x3a, 0x6a, 0xfc, 0x53, 0x8e, 0xfa, 0xc1,
|
||||
};
|
||||
const unsigned int ecdsa_pub_key_len = 91;
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* @file mcuboot_config.h
|
||||
* @brief Конфигурация bootutil (MCUboot) для загрузчика TFT.
|
||||
*
|
||||
* В отличие от шаблона NXP (nxp_mcux_sdk/include/mcuboot_config/mcuboot_config.h)
|
||||
* задаёт финальные макросы напрямую, без Kconfig-подобной прослойки —
|
||||
* решения зафиксированы в firmware/bootloader/PLAN.md, Фаза 2:
|
||||
*
|
||||
* - Direct-XIP с revert: два слота, оба могут содержать валидный образ,
|
||||
* bootutil выбирает более новую валидную версию; если она ни разу не
|
||||
* подтверждена (confirm) — следующая загрузка откатится на предыдущую
|
||||
* (см. MCUBOOT_DIRECT_XIP_REVERT).
|
||||
* - ECDSA P-256 + TinyCrypt — компактный, полностью вендорен в репозитории
|
||||
* (sdk/middleware/mcuboot_opensource/ext/tinycrypt), в отличие от
|
||||
* mbedTLS (не вендорен, потребовал бы ~8000+ новых строк).
|
||||
* - FIH профиль LOW — часть защиты bootutil (double-read сравнений) без
|
||||
* RNG-задержки (та требует mbedTLS-энтропию, доступно только в профиле
|
||||
* HIGH — не наш случай).
|
||||
* - Heap НЕ нужен: malloc/free в bootutil (loader.c) вызываются только в
|
||||
* swap-режиме, недостижимы под MCUBOOT_DIRECT_XIP.
|
||||
*/
|
||||
|
||||
#ifndef MCUBOOT_CONFIG_H_
|
||||
#define MCUBOOT_CONFIG_H_
|
||||
|
||||
/* ── Схема подписи ─────────────────────────────────────────────────────── */
|
||||
#define MCUBOOT_SIGN_EC256
|
||||
|
||||
/* ── Крипто-бэкенд ─────────────────────────────────────────────────────── */
|
||||
#define MCUBOOT_USE_TINYCRYPT
|
||||
|
||||
/* ── Режим обновления ─────────────────────────────────────────────────── */
|
||||
#define MCUBOOT_DIRECT_XIP
|
||||
#define MCUBOOT_DIRECT_XIP_REVERT
|
||||
|
||||
/* Проверять подпись активного слота при каждой загрузке, не только при
|
||||
* установке нового образа. */
|
||||
#define MCUBOOT_VALIDATE_PRIMARY_SLOT
|
||||
|
||||
/* ── Образы ────────────────────────────────────────────────────────────── */
|
||||
#define MCUBOOT_IMAGE_NUMBER 1
|
||||
|
||||
/* ── Flash-абстракция ─────────────────────────────────────────────────── */
|
||||
#define MCUBOOT_USE_FLASH_AREA_GET_SECTORS
|
||||
|
||||
/* Slot A/Б = 2 МБ (см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md), сектор
|
||||
* W25Qxx = 4 КБ → 2 МБ / 4 КБ = 512 секторов на слот. */
|
||||
#define MCUBOOT_MAX_IMG_SECTORS 512
|
||||
|
||||
/* ── Fault injection hardening ────────────────────────────────────────── */
|
||||
#define MCUBOOT_FIH_PROFILE_LOW
|
||||
|
||||
/* ── Логирование — отключено, BOOT_LOG_* становятся no-op (bootutil_log.h) */
|
||||
|
||||
/* ── Watchdog — не используется в bootloader ─────────────────────────── */
|
||||
#define MCUBOOT_WATCHDOG_FEED() \
|
||||
do \
|
||||
{ \
|
||||
} while (0)
|
||||
|
||||
#endif /* MCUBOOT_CONFIG_H_ */
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* @file mcuboot_logging.h
|
||||
* @brief Логирование bootutil — не используется (MCUBOOT_HAVE_LOGGING не
|
||||
* определён в mcuboot_config.h, BOOT_LOG_* становятся no-op через
|
||||
* bootutil_log.h). Этот заголовок обязателен к существованию —
|
||||
* часть заголовков bootutil (bootutil/crypto/sha.h) включает его
|
||||
* безусловно, независимо от MCUBOOT_HAVE_LOGGING.
|
||||
*/
|
||||
|
||||
#ifndef MCUBOOT_LOGGING_H_
|
||||
#define MCUBOOT_LOGGING_H_
|
||||
|
||||
#define MCUBOOT_LOG_MODULE_DECLARE(domain)
|
||||
#define MCUBOOT_LOG_MODULE_REGISTER(domain)
|
||||
|
||||
#define MCUBOOT_LOG_ERR(...)
|
||||
#define MCUBOOT_LOG_WRN(...)
|
||||
#define MCUBOOT_LOG_INF(...)
|
||||
#define MCUBOOT_LOG_DBG(...)
|
||||
|
||||
#endif /* MCUBOOT_LOGGING_H_ */
|
||||
26
firmware/bootloader/mcuboot_port/sysflash/sysflash.h
Normal file
26
firmware/bootloader/mcuboot_port/sysflash/sysflash.h
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* @file sysflash.h
|
||||
* @brief Отображение логических слотов bootutil на flash-area ID.
|
||||
*
|
||||
* MCUBOOT_IMAGE_NUMBER=1 → два ID: Slot A (primary=0), Slot Б (secondary=1).
|
||||
* Без scratch — Direct-XIP не использует область подкачки. Смещения/размеры
|
||||
* самих областей заданы в flash_map_backend.c (см.
|
||||
* docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md).
|
||||
*/
|
||||
|
||||
#ifndef SYSFLASH_H_
|
||||
#define SYSFLASH_H_
|
||||
|
||||
#include "mcuboot_config/mcuboot_config.h"
|
||||
|
||||
#define FLASH_AREA_IMAGE_PRIMARY(x) (((x) == 0) ? 0 : 255)
|
||||
#define FLASH_AREA_IMAGE_SECONDARY(x) (((x) == 0) ? 1 : 255)
|
||||
|
||||
#define MCUBOOT_IMAGE_SLOT_NUMBER (MCUBOOT_IMAGE_NUMBER * 2)
|
||||
|
||||
/** @brief Единственное flash-устройство в системе — W25Qxx через FlexSPI. */
|
||||
#define FLASH_DEVICE_ID 1
|
||||
|
||||
int flash_area_id_from_multi_image_slot(int image_index, int slot);
|
||||
|
||||
#endif /* SYSFLASH_H_ */
|
||||
109
firmware/bootloader/src/boot_select.c
Normal file
109
firmware/bootloader/src/boot_select.c
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* @file boot_select.c
|
||||
* @brief bootutil boot_go() + прыжок в выбранный образ.
|
||||
*
|
||||
* Референс — sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/boot.c::do_boot():
|
||||
* та же последовательность (flash_device_base → вычислить адрес vector table →
|
||||
* cleanup → __set_MSP → __ISB → прыжок на Reset_Handler), CMSIS-интринсики, без
|
||||
* ассемблера. cleanup_before_jump()/VTOR добавлены в Фазе 3 — см. её docstring
|
||||
* про найденный на железе баг (прыжок сразу после bsp_usb_cdc_init()).
|
||||
*/
|
||||
|
||||
#include "boot_select.h"
|
||||
|
||||
#include "bootutil/bootutil.h"
|
||||
#include "bootutil/fault_injection_hardening.h"
|
||||
#include "flash_map.h"
|
||||
#include "fsl_common.h"
|
||||
|
||||
struct arm_vector_table
|
||||
{
|
||||
uint32_t msp;
|
||||
uint32_t reset;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Вернуть NVIC/SysTick в состояние "как после аппаратного сброса"
|
||||
* перед прыжком — целевой образ не должен унаследовать прерывания,
|
||||
* включённые bootloader'ом.
|
||||
*
|
||||
* Найдено на реальном железе (Фаза 3): Slot A с уже валидным подтверждённым
|
||||
* образом не загружался, когда прыжок происходил сразу после
|
||||
* bsp_usb_cdc_init() (USB ещё в процессе enumeration — прерывания частые), но
|
||||
* загружался, когда прыжок происходил позже (после цикла ожидания SD — USB
|
||||
* уже в устоявшемся состоянии). Причина: bootloader (в отличие от Фазы 1/2,
|
||||
* где до прыжка включался только bsp_qspi_init() — без единого постоянно
|
||||
* включённого NVIC IRQ) теперь включает USB CDC и, при вставленной SD,
|
||||
* USDHC — оба взводят свои NVIC IRQ. jump_to_image() не переключал VTOR —
|
||||
* прерывание, сработавшее в узком окне между прыжком и тем, как целевой
|
||||
* образ успеет настроить свою таблицу векторов в Reset_Handler/SystemInit(),
|
||||
* уходило по ещё активной (bootloader'овской) таблице векторов с чужим
|
||||
* стеком/контекстом.
|
||||
*
|
||||
* Портировано по мотивам cleanup()/SBL_DisablePeripherals() в референсном
|
||||
* sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/boot.c::do_boot() — не
|
||||
* скопировано напрямую (SBL_DisablePeripherals — extern, платформенно-
|
||||
* специфичная функция, не вендоренная в нашем дереве); здесь общий,
|
||||
* не завязанный на конкретную периферию эквивалент через NVIC/SysTick.
|
||||
*
|
||||
* НЕ трогает PRIMASK (в отличие от более ранней, откаченной в Фазе 2
|
||||
* версии) — SysTick_Handler целевого образа должен сработать после того как
|
||||
* образ сам вызовет bsp_tick_init(), а PRIMASK обычный Reset_Handler не
|
||||
* восстанавливает (см. bsp_delay()-зависание, найденное в Фазе 2).
|
||||
* Отключение конкретных источников (NVIC ICER/ICPR, SysTick->CTRL) — не
|
||||
* то же самое, что глобальная маскировка: раз выключенный SysTick просто не
|
||||
* тикает, пока образ не включит его сам, и не блокирует его же будущий
|
||||
* запуск.
|
||||
*/
|
||||
static void cleanup_before_jump(void)
|
||||
{
|
||||
for (uint32_t i = 0U; i < (sizeof(NVIC->ICER) / sizeof(NVIC->ICER[0])); i++)
|
||||
{
|
||||
NVIC->ICER[i] = 0xFFFFFFFFU; /* запретить все внешние IRQ */
|
||||
NVIC->ICPR[i] = 0xFFFFFFFFU; /* сбросить pending — не унаследовать флаг */
|
||||
}
|
||||
|
||||
SysTick->CTRL = 0U; /* SysTick — не в NVIC->ICER, отдельный системный таймер */
|
||||
|
||||
__DSB();
|
||||
__ISB();
|
||||
}
|
||||
|
||||
static void jump_to_image(const struct boot_rsp *p_rsp)
|
||||
{
|
||||
uintptr_t flash_base;
|
||||
if (flash_device_base(p_rsp->br_flash_dev_id, &flash_base) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const struct arm_vector_table *p_vt =
|
||||
(const struct arm_vector_table *) (flash_base + p_rsp->br_image_off +
|
||||
p_rsp->br_hdr->ih_hdr_size);
|
||||
|
||||
cleanup_before_jump();
|
||||
|
||||
/* Образ сам переставит VTOR в своём Reset_Handler/SystemInit() — но до
|
||||
* этого момента (первые же инструкции после прыжка) он уже должен быть
|
||||
* валиден, на случай если что-то прервёт выполнение раньше. */
|
||||
SCB->VTOR = (uint32_t) p_vt;
|
||||
|
||||
/* Намеренно НЕ __disable_irq()/PRIMASK здесь — см. cleanup_before_jump(). */
|
||||
__set_CONTROL(0U);
|
||||
__set_MSP(p_vt->msp);
|
||||
__ISB();
|
||||
((void (*)(void)) p_vt->reset)();
|
||||
}
|
||||
|
||||
void boot_select_and_jump(void)
|
||||
{
|
||||
struct boot_rsp rsp;
|
||||
fih_ret fih_rc = boot_go(&rsp);
|
||||
|
||||
if (!FIH_EQ(fih_rc, FIH_SUCCESS))
|
||||
{
|
||||
return; /* нет валидного образа — main.c продолжит ping/pong-цикл */
|
||||
}
|
||||
|
||||
jump_to_image(&rsp); /* при успехе не возвращается */
|
||||
}
|
||||
21
firmware/bootloader/src/boot_select.h
Normal file
21
firmware/bootloader/src/boot_select.h
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* @file boot_select.h
|
||||
* @brief Выбор и запуск образа tft_app через bootutil (Direct-XIP).
|
||||
*/
|
||||
|
||||
#ifndef BOOT_SELECT_H_
|
||||
#define BOOT_SELECT_H_
|
||||
|
||||
/**
|
||||
* @brief Выбрать образ (bootutil boot_go, Direct-XIP) и прыгнуть в него.
|
||||
*
|
||||
* При успехе не возвращается — управление переходит в выбранный образ.
|
||||
* При провале (нет валидного образа ни в одном слоте, или оба слота стёрты
|
||||
* bootutil'ом из-за незавершённого revert) — возвращается, чтобы main.c
|
||||
* мог продолжить в ping/pong-цикл (задел на состояние "жду SD" Фазы 3).
|
||||
*
|
||||
* @pre bsp_qspi_init() уже вызван.
|
||||
*/
|
||||
void boot_select_and_jump(void);
|
||||
|
||||
#endif /* BOOT_SELECT_H_ */
|
||||
257
firmware/bootloader/src/cli.c
Normal file
257
firmware/bootloader/src/cli.c
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
/**
|
||||
* @file cli.c
|
||||
* @brief IO-слой и диспатчер сообщений для bootloader.
|
||||
*
|
||||
* Транспорт: USB CDC ACM через bsp_usb_cdc.
|
||||
*
|
||||
* Парсинг минималистичный: strstr по фиксированным полям (тот же подход,
|
||||
* что и в firmware_test/src/cli.c) — cJSON не используется намеренно, схема
|
||||
* входящих сообщений фиксирована.
|
||||
*
|
||||
* Входящие типы (Фаза 1):
|
||||
* "cmd" → handle_cmd() → protocol_send_pong() / protocol_send_version_response()
|
||||
*
|
||||
* [Фаза 4] Команды "smoke_status"/"qspi_info" — переспросить результат
|
||||
* boot-time smoke-теста SDRAM/SEMC и опознанный чип QSPI flash в любой
|
||||
* момент сессии (сами события шлются один раз рано, до открытия хостом
|
||||
* порта, и почти наверняка теряются — см. protocol.h).
|
||||
*
|
||||
* [DEV-ONLY, Фаза 4] Команда "sdram_test" — под BOOTLOADER_DEV_DIAGNOSTICS
|
||||
* (только Debug, см. CMakeLists.txt): запускает dev_sdram_test_run(), которая
|
||||
* блокирует главный цикл на ~4 с (пройденный прогон на железе: configure ~0,
|
||||
* address_bus <1 мс, data_bus ~130 мс, sequential ~3.7 с, retention ~400 мс).
|
||||
* Отсутствует в Release/HAB-бинаре.
|
||||
*
|
||||
* Добавление новой команды типа "cmd":
|
||||
* 1. Добавить ветку if (strcmp(cmd_name, "FOO") == 0) в handle_cmd().
|
||||
*/
|
||||
|
||||
#include "cli.h"
|
||||
|
||||
#include "bsp/usb_cdc.h"
|
||||
#include "protocol.h"
|
||||
|
||||
#ifdef BOOTLOADER_DEV_DIAGNOSTICS
|
||||
#include "dev_sdram_test.h"
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── Ключи полей JSON ──────────────────────────────────────────────────── */
|
||||
|
||||
static const char K_FIELD_TYPE[] = "\"type\"";
|
||||
static const char K_FIELD_CMD[] = "\"cmd\"";
|
||||
|
||||
/** @brief Буфер непрочитанного остатка chunk после вызова process_line(). */
|
||||
static uint8_t g_s_chunk_buf[CLI_LINE_BUF_SIZE];
|
||||
static size_t g_s_chunk_len = 0U;
|
||||
static size_t g_s_chunk_pos = 0U;
|
||||
|
||||
/* ── RX line buffer ────────────────────────────────────────────────────── */
|
||||
|
||||
static uint8_t g_s_line_buf[CLI_LINE_BUF_SIZE];
|
||||
static size_t g_s_line_len = 0U;
|
||||
|
||||
/* ── Парсинг полей ─────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @brief Извлечь строковое значение в кавычках после двоеточия.
|
||||
*
|
||||
* @param[in] p_after_key Позиция сразу после ключа в строке JSON.
|
||||
* @param[out] p_out Буфер для результата.
|
||||
* @param[in] out_size Размер p_out (включая место под '\0').
|
||||
* @return true если значение найдено и помещается в p_out.
|
||||
*/
|
||||
static bool extract_string_value(const char *p_after_key, char *p_out, size_t out_size)
|
||||
{
|
||||
const char *colon = strchr(p_after_key, ':');
|
||||
if (colon == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *open_q = strchr(colon + 1U, '"');
|
||||
if (open_q == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
open_q++;
|
||||
|
||||
const char *close_q = strchr(open_q, '"');
|
||||
if (close_q == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t len = (size_t) (close_q - open_q);
|
||||
if (len == 0U || len >= out_size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(p_out, open_q, len);
|
||||
p_out[len] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Извлечь значение поля "type".
|
||||
*/
|
||||
static bool parse_type_field(const char *p_line, char *p_out, size_t out_size)
|
||||
{
|
||||
const char *key = strstr(p_line, K_FIELD_TYPE);
|
||||
if (key == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return extract_string_value(key + sizeof(K_FIELD_TYPE) - 1U, p_out, out_size);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Извлечь значение поля "cmd".
|
||||
*/
|
||||
static bool parse_cmd_field(const char *p_line, char *p_out, size_t out_size)
|
||||
{
|
||||
const char *key = strstr(p_line, K_FIELD_CMD);
|
||||
if (key == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return extract_string_value(key + sizeof(K_FIELD_CMD) - 1U, p_out, out_size);
|
||||
}
|
||||
|
||||
/* ── Обработчики входящих сообщений ────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @brief Обработать сообщение {"type":"cmd",...}.
|
||||
*/
|
||||
static void handle_cmd(const char *p_line)
|
||||
{
|
||||
const uint8_t MAX_CMD_LEN = 32U;
|
||||
char cmd_name[MAX_CMD_LEN];
|
||||
|
||||
if (!parse_cmd_field(p_line, cmd_name, sizeof(cmd_name)))
|
||||
{
|
||||
protocol_send_error("PARSE_ERR");
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(cmd_name, "ping") == 0)
|
||||
{
|
||||
protocol_send_pong();
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(cmd_name, "get_version") == 0)
|
||||
{
|
||||
protocol_send_version_response();
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(cmd_name, "wdog") == 0)
|
||||
{
|
||||
protocol_send_wdog_status();
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(cmd_name, "smoke_status") == 0)
|
||||
{
|
||||
protocol_send_smoke_status();
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(cmd_name, "qspi_info") == 0)
|
||||
{
|
||||
protocol_send_qspi_info();
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef BOOTLOADER_DEV_DIAGNOSTICS
|
||||
if (strcmp(cmd_name, "sdram_test") == 0)
|
||||
{
|
||||
dev_sdram_test_run();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
protocol_send_error("UNKNOWN_CMD");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Диспатчить накопленную строку по полю "type".
|
||||
*/
|
||||
static void process_line(const char *p_line)
|
||||
{
|
||||
const uint8_t MAX_TYPE_LEN = 16U;
|
||||
char msg_type[MAX_TYPE_LEN];
|
||||
|
||||
if (!parse_type_field(p_line, msg_type, sizeof(msg_type)))
|
||||
{
|
||||
protocol_send_error("PARSE_ERR");
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(msg_type, "cmd") == 0)
|
||||
{
|
||||
handle_cmd(p_line);
|
||||
return;
|
||||
}
|
||||
|
||||
protocol_send_error("UNKNOWN_CMD");
|
||||
}
|
||||
|
||||
/* ── Public API ────────────────────────────────────────────────────────── */
|
||||
|
||||
void cli_init(void)
|
||||
{
|
||||
g_s_line_len = 0U;
|
||||
g_s_chunk_len = 0U;
|
||||
g_s_chunk_pos = 0U;
|
||||
}
|
||||
|
||||
void cli_send(const char *p_resp)
|
||||
{
|
||||
bsp_usb_cdc_write((const uint8_t *) p_resp, strlen(p_resp));
|
||||
}
|
||||
|
||||
void cli_process(void)
|
||||
{
|
||||
if (g_s_chunk_pos >= g_s_chunk_len)
|
||||
{
|
||||
g_s_chunk_len = bsp_usb_cdc_read(g_s_chunk_buf, sizeof(g_s_chunk_buf));
|
||||
g_s_chunk_pos = 0U;
|
||||
}
|
||||
|
||||
while (g_s_chunk_pos < g_s_chunk_len)
|
||||
{
|
||||
uint8_t byte = g_s_chunk_buf[g_s_chunk_pos];
|
||||
g_s_chunk_pos++;
|
||||
|
||||
if (g_s_line_len >= (CLI_LINE_BUF_SIZE - 1U))
|
||||
{
|
||||
g_s_line_len = 0U;
|
||||
protocol_send_error("LINE_TOO_LONG");
|
||||
return;
|
||||
}
|
||||
|
||||
if (byte == (uint8_t) '\n')
|
||||
{
|
||||
if (g_s_line_len > 0U && g_s_line_buf[g_s_line_len - 1U] == (uint8_t) '\r')
|
||||
{
|
||||
g_s_line_len--;
|
||||
}
|
||||
g_s_line_buf[g_s_line_len] = '\0';
|
||||
size_t completed_len = g_s_line_len;
|
||||
g_s_line_len = 0U; /* ← сбросить ДО process_line */
|
||||
if (completed_len > 0U)
|
||||
{
|
||||
process_line((const char *) g_s_line_buf);
|
||||
}
|
||||
}
|
||||
|
||||
g_s_line_buf[g_s_line_len] = byte;
|
||||
g_s_line_len++;
|
||||
}
|
||||
}
|
||||
50
firmware/bootloader/src/cli.h
Normal file
50
firmware/bootloader/src/cli.h
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* @file cli.h
|
||||
* @brief IO-слой CLI для bootloader.
|
||||
*
|
||||
* Транспорт: USB CDC ACM (bsp_usb_cdc) — единственный канал.
|
||||
* Протокол: JSON-lines, каждая строка завершается '\n'. Урезанное
|
||||
* подмножество протокола firmware_test (firmware/test/src/cli.h).
|
||||
*
|
||||
* Входящие типы (Фаза 1):
|
||||
* {"type":"cmd", "cmd":"ping"}
|
||||
* {"type":"cmd", "cmd":"get_version"}
|
||||
*
|
||||
* Исходящие события формируются через protocol.h, а не напрямую через cli_send().
|
||||
* cli_send() остаётся публичным: его использует protocol.c как единственную
|
||||
* точку вывода.
|
||||
*/
|
||||
|
||||
#ifndef CLI_H_
|
||||
#define CLI_H_
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/** @brief Максимальная длина входящей JSON-строки включая '\n'. */
|
||||
#define CLI_LINE_BUF_SIZE 128U
|
||||
|
||||
/**
|
||||
* @brief Инициализировать CLI. Сбрасывает внутренний буфер строки.
|
||||
*
|
||||
* Вызывать после bsp_usb_cdc_init() и до первого cli_process().
|
||||
*/
|
||||
void cli_init(void);
|
||||
|
||||
/**
|
||||
* @brief Отправить готовую JSON-строку через USB CDC.
|
||||
*
|
||||
* @param[in] p_resp NUL-terminated строка, завершённая '\n'.
|
||||
*
|
||||
* @note Неблокирующий. Если TX занят — запись теряется.
|
||||
*/
|
||||
void cli_send(const char *p_resp);
|
||||
|
||||
/**
|
||||
* @brief Обработать входящие байты, диспатчить сообщение при получении '\n'.
|
||||
*
|
||||
* Вызывать в главном цикле после bsp_usb_cdc_poll().
|
||||
* Неблокирующий: если данных нет — возвращается немедленно.
|
||||
*/
|
||||
void cli_process(void);
|
||||
|
||||
#endif /* CLI_H_ */
|
||||
282
firmware/bootloader/src/dev_sdram_test.c
Normal file
282
firmware/bootloader/src/dev_sdram_test.c
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
/**
|
||||
* @file dev_sdram_test.c
|
||||
* @brief [DEV-ONLY] Реализация глубокого теста SDRAM (см. dev_sdram_test.h).
|
||||
*
|
||||
* Фазы 1:1 портированы из firmware/test/src/tests/test_sdram.c (тот же
|
||||
* алгоритм, то же покрытие) — не переизобретаются, чтобы результат был
|
||||
* сопоставим с уже доверенным тестом DCD-пути. Отличия от оригинала:
|
||||
* - sdram_test_init() там предполагал DCD; здесь сама зовёт
|
||||
* bsp_sdram_configure() — это и есть предмет проверки.
|
||||
* - test_module_t/test_result_t (инфраструктура тест-раннера firmware_test)
|
||||
* не используются — bootloader её не имеет; результат каждой фазы уходит
|
||||
* отдельным CDC-событием по ходу прогона, не одним отчётом в конце.
|
||||
* - добавлено кормление watchdog (bsp_wdog_refresh()) на той же частоте,
|
||||
* что и опрос CDC — без него более медленный прогон рисковал бы не
|
||||
* пережить таймаут WDOG (10 с, main.c) и словить сброс посреди фазы
|
||||
* sequential (по факту прогон ~4 с, запас большой — см. dev_sdram_test.h).
|
||||
*/
|
||||
|
||||
#include "dev_sdram_test.h"
|
||||
|
||||
#include "bsp/sdram.h"
|
||||
#include "bsp/tick.h"
|
||||
#include "bsp/usb_cdc.h"
|
||||
#include "bsp/wdog.h"
|
||||
#include "fsl_common.h"
|
||||
#include "protocol.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ── Константы (те же значения, что в firmware_test/test_sdram.c) ───────── */
|
||||
|
||||
/** @brief Интервал вызова pump() при записи/чтении, байт. */
|
||||
#define SDRAM_USB_POLL_INTERVAL_BYTES 0x1000U
|
||||
|
||||
/** @brief Ширина walking ones паттерна, бит. */
|
||||
#define SDRAM_WALKING_ONES_BITS 8U
|
||||
|
||||
/** @brief Количество адресных бит теста address_bus (13 row+9 col+2 bank). */
|
||||
#define SDRAM_ADDR_BUS_BITS 24U
|
||||
|
||||
/** @brief Размер кэш-линии Cortex-M7, байт. */
|
||||
#define SDRAM_CACHE_LINE_BYTES 32U
|
||||
|
||||
/** @brief Размер фазы sequential, байт (2 MB). */
|
||||
#define SDRAM_SEQUENTIAL_SIZE 0x00200000UL
|
||||
|
||||
/** @brief Размер фазы retention, байт (256 KB). */
|
||||
#define SDRAM_RETENTION_SIZE 0x00040000UL
|
||||
|
||||
/** @brief Задержка фазы retention, мс (MT48LC16M16A2: авто-refresh 64 мс, ≈3 периода). */
|
||||
#define SDRAM_RETENTION_DELAY_MS 200U
|
||||
|
||||
/** @brief Шаг ожидания в фазе retention (для pump()), мс. */
|
||||
#define SDRAM_RETENTION_POLL_STEP_MS 10U
|
||||
|
||||
/* ── Локальные типы ────────────────────────────────────────────────────── */
|
||||
|
||||
/** @brief Информация о первой ошибке паттерн-прохода. fail_addr==0 — нет ошибки. */
|
||||
typedef struct
|
||||
{
|
||||
uint32_t fail_addr;
|
||||
uint8_t expected;
|
||||
uint8_t got;
|
||||
} sdram_fail_info_t;
|
||||
|
||||
typedef uint8_t (*pattern_fn_t)(uint32_t offset);
|
||||
|
||||
/* ── Паттерн-функции ───────────────────────────────────────────────────── */
|
||||
|
||||
static uint8_t pattern_walking_ones(uint32_t offset)
|
||||
{
|
||||
return (uint8_t) (1U << (offset % SDRAM_WALKING_ONES_BITS));
|
||||
}
|
||||
|
||||
static uint8_t pattern_walking_ones_inv(uint32_t offset)
|
||||
{
|
||||
return (uint8_t) (~(1U << (offset % SDRAM_WALKING_ONES_BITS)) & 0xFFU);
|
||||
}
|
||||
|
||||
static uint8_t pattern_addr(uint32_t offset)
|
||||
{
|
||||
return (uint8_t) (offset & 0xFFU);
|
||||
}
|
||||
|
||||
static uint8_t pattern_addr_inv(uint32_t offset)
|
||||
{
|
||||
return (uint8_t) (~offset & 0xFFU);
|
||||
}
|
||||
|
||||
/* ── Вспомогательные функции ───────────────────────────────────────────── */
|
||||
|
||||
/** @brief Опрос CDC + кормление watchdog. Вызывать не реже раза в ~4 KB. */
|
||||
static void pump(void)
|
||||
{
|
||||
bsp_usb_cdc_poll();
|
||||
bsp_wdog_refresh();
|
||||
}
|
||||
|
||||
static void flush_dcache(uint32_t size)
|
||||
{
|
||||
uint32_t *const P_BASE = (uint32_t *) BSP_SDRAM_TEST_BASE_ADDR;
|
||||
|
||||
SCB_CleanDCache_by_Addr(P_BASE, (int32_t) size);
|
||||
SCB_InvalidateDCache_by_Addr(P_BASE, (int32_t) size);
|
||||
__DSB();
|
||||
}
|
||||
|
||||
/** @brief Сброс одной кэш-линии по произвольному байтовому адресу (для address_bus). */
|
||||
static void flush_dcache_line_at(uint32_t byte_addr)
|
||||
{
|
||||
uint32_t *const P_LINE = (uint32_t *) (byte_addr & ~(SDRAM_CACHE_LINE_BYTES - 1U));
|
||||
|
||||
SCB_CleanDCache_by_Addr(P_LINE, (int32_t) SDRAM_CACHE_LINE_BYTES);
|
||||
SCB_InvalidateDCache_by_Addr(P_LINE, (int32_t) SDRAM_CACHE_LINE_BYTES);
|
||||
__DSB();
|
||||
}
|
||||
|
||||
static void delay_with_pump(uint32_t ms)
|
||||
{
|
||||
const uint32_t START_MS = bsp_tick_get_ms();
|
||||
|
||||
while ((bsp_tick_get_ms() - START_MS) < ms)
|
||||
{
|
||||
bsp_delay(SDRAM_RETENTION_POLL_STEP_MS);
|
||||
pump();
|
||||
}
|
||||
}
|
||||
|
||||
static void write_pattern(uint32_t size, pattern_fn_t p_fn)
|
||||
{
|
||||
volatile uint8_t *const P_BASE = (volatile uint8_t *) BSP_SDRAM_TEST_BASE_ADDR;
|
||||
|
||||
for (uint32_t i = 0U; i < size; i++)
|
||||
{
|
||||
if ((i & (SDRAM_USB_POLL_INTERVAL_BYTES - 1U)) == 0U)
|
||||
{
|
||||
pump();
|
||||
}
|
||||
P_BASE[i] = p_fn(i);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Верифицировать паттерн. Останавливается на первом несовпадении. */
|
||||
static bool verify_pattern(uint32_t size, pattern_fn_t p_fn, sdram_fail_info_t *p_fail)
|
||||
{
|
||||
volatile const uint8_t *const P_BASE = (volatile const uint8_t *) BSP_SDRAM_TEST_BASE_ADDR;
|
||||
|
||||
for (uint32_t i = 0U; i < size; i++)
|
||||
{
|
||||
if ((i & (SDRAM_USB_POLL_INTERVAL_BYTES - 1U)) == 0U)
|
||||
{
|
||||
pump();
|
||||
}
|
||||
|
||||
const uint8_t EXPECTED = p_fn(i);
|
||||
const uint8_t GOT = P_BASE[i];
|
||||
|
||||
if (GOT != EXPECTED)
|
||||
{
|
||||
p_fail->fail_addr = BSP_SDRAM_TEST_BASE_ADDR + i;
|
||||
p_fail->expected = EXPECTED;
|
||||
p_fail->got = GOT;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @brief Один паттерн-проход: запись → flush → верификация. */
|
||||
static bool run_pass(uint32_t size, pattern_fn_t p_fn, sdram_fail_info_t *p_fail)
|
||||
{
|
||||
write_pattern(size, p_fn);
|
||||
flush_dcache(size);
|
||||
return verify_pattern(size, p_fn, p_fail);
|
||||
}
|
||||
|
||||
/* ── Фазы теста (см. dev_sdram_test.h) ───────────────────────────────────── */
|
||||
|
||||
/** @brief Фаза address_bus: 24 адресных бита, точечный флаш на каждую запись. */
|
||||
static bool run_phase_address_bus(sdram_fail_info_t *p_fail)
|
||||
{
|
||||
volatile uint8_t *const P_BASE = (volatile uint8_t *) BSP_SDRAM_TEST_BASE_ADDR;
|
||||
|
||||
for (uint32_t bit = 0U; bit < SDRAM_ADDR_BUS_BITS; bit++)
|
||||
{
|
||||
const uint32_t OFFSET = (1UL << bit);
|
||||
const uint8_t PATTERN = (uint8_t) (bit + 1U);
|
||||
|
||||
P_BASE[OFFSET] = PATTERN;
|
||||
flush_dcache_line_at(BSP_SDRAM_TEST_BASE_ADDR + OFFSET);
|
||||
|
||||
const uint8_t GOT = P_BASE[OFFSET];
|
||||
if (GOT != PATTERN)
|
||||
{
|
||||
p_fail->fail_addr = BSP_SDRAM_TEST_BASE_ADDR + OFFSET;
|
||||
p_fail->expected = PATTERN;
|
||||
p_fail->got = GOT;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @brief Фаза data_bus: walking ones + инверсия, 64 KB. */
|
||||
static bool run_phase_data_bus(sdram_fail_info_t *p_fail)
|
||||
{
|
||||
return run_pass(BSP_SDRAM_TEST_FAST_SIZE, pattern_walking_ones, p_fail) &&
|
||||
run_pass(BSP_SDRAM_TEST_FAST_SIZE, pattern_walking_ones_inv, p_fail);
|
||||
}
|
||||
|
||||
/** @brief Фаза sequential: address pattern + инверсия, 2 MB. */
|
||||
static bool run_phase_sequential(sdram_fail_info_t *p_fail)
|
||||
{
|
||||
return run_pass(SDRAM_SEQUENTIAL_SIZE, pattern_addr, p_fail) &&
|
||||
run_pass(SDRAM_SEQUENTIAL_SIZE, pattern_addr_inv, p_fail);
|
||||
}
|
||||
|
||||
/** @brief Фаза retention: запись → flush → 200 мс → verify, 256 KB. */
|
||||
static bool run_phase_retention(sdram_fail_info_t *p_fail)
|
||||
{
|
||||
write_pattern(SDRAM_RETENTION_SIZE, pattern_addr);
|
||||
flush_dcache(SDRAM_RETENTION_SIZE);
|
||||
delay_with_pump(SDRAM_RETENTION_DELAY_MS);
|
||||
return verify_pattern(SDRAM_RETENTION_SIZE, pattern_addr, p_fail);
|
||||
}
|
||||
|
||||
/* ── Оркестрация ──────────────────────────────────────────────────────── */
|
||||
|
||||
static void report_phase(const char *p_name, bool pass, uint32_t start_ms, const sdram_fail_info_t *p_fail)
|
||||
{
|
||||
const uint32_t DURATION_MS = bsp_tick_get_ms() - start_ms;
|
||||
const uint32_t FAIL_ADDR = (p_fail != NULL) ? p_fail->fail_addr : 0U;
|
||||
const uint8_t EXPECTED = (p_fail != NULL) ? p_fail->expected : 0U;
|
||||
const uint8_t GOT = (p_fail != NULL) ? p_fail->got : 0U;
|
||||
|
||||
protocol_send_sdram_test_phase(p_name, pass, DURATION_MS, FAIL_ADDR, EXPECTED, GOT);
|
||||
}
|
||||
|
||||
void dev_sdram_test_run(void)
|
||||
{
|
||||
const uint32_t CONFIGURE_START_MS = bsp_tick_get_ms();
|
||||
|
||||
const bool CONFIGURED = (bsp_sdram_configure() == BSP_OK) && (bsp_sdram_init() == BSP_OK);
|
||||
report_phase("configure", CONFIGURED, CONFIGURE_START_MS, NULL);
|
||||
if (!CONFIGURED)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t OVERALL_START_MS = bsp_tick_get_ms();
|
||||
sdram_fail_info_t fail = { 0U, 0U, 0U };
|
||||
bool is_ok;
|
||||
uint32_t phase_start_ms;
|
||||
|
||||
phase_start_ms = bsp_tick_get_ms();
|
||||
is_ok = run_phase_address_bus(&fail);
|
||||
report_phase("address_bus", is_ok, phase_start_ms, is_ok ? NULL : &fail);
|
||||
|
||||
if (is_ok)
|
||||
{
|
||||
phase_start_ms = bsp_tick_get_ms();
|
||||
is_ok = run_phase_data_bus(&fail);
|
||||
report_phase("data_bus", is_ok, phase_start_ms, is_ok ? NULL : &fail);
|
||||
}
|
||||
|
||||
if (is_ok)
|
||||
{
|
||||
phase_start_ms = bsp_tick_get_ms();
|
||||
is_ok = run_phase_sequential(&fail);
|
||||
report_phase("sequential", is_ok, phase_start_ms, is_ok ? NULL : &fail);
|
||||
}
|
||||
|
||||
if (is_ok)
|
||||
{
|
||||
phase_start_ms = bsp_tick_get_ms();
|
||||
is_ok = run_phase_retention(&fail);
|
||||
report_phase("retention", is_ok, phase_start_ms, is_ok ? NULL : &fail);
|
||||
}
|
||||
|
||||
report_phase("summary", is_ok, OVERALL_START_MS, NULL);
|
||||
}
|
||||
45
firmware/bootloader/src/dev_sdram_test.h
Normal file
45
firmware/bootloader/src/dev_sdram_test.h
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* @file dev_sdram_test.h
|
||||
* @brief [DEV-ONLY] Глубокий тест SDRAM через bsp_sdram_configure() (Фаза 4).
|
||||
*
|
||||
* НЕ для production — компилируется только в Debug
|
||||
* (BOOTLOADER_DEV_DIAGNOSTICS, см. CMakeLists.txt); отсутствует в HAB
|
||||
* Release-бинаре.
|
||||
*
|
||||
* Цель — не заменить лёгкий boot-time smoke-test (main.c, "smoke_pass"/
|
||||
* "smoke_fail"), а дать ту же глубину проверки, что уже доверена
|
||||
* firmware_test/src/tests/test_sdram.c (4 фазы, покрывают адресную шину,
|
||||
* шину данных, coupling между ячейками и refresh-timing), но против нового
|
||||
* C-порта DCD (bsp_sdram_configure()), а не против DCD напрямую — чтобы
|
||||
* убедиться, что порт поднимает память так же надёжно, как проверенный
|
||||
* годами в производстве DCD.
|
||||
*
|
||||
* Вызывается по CDC-команде {"type":"cmd","cmd":"sdram_test"} (см. cli.c).
|
||||
* Блокирует главный цикл на время прогона (~4 с, см. таймингы ниже — реально
|
||||
* измерено на железе, а не оценка) — сама кормит watchdog и опрашивает CDC по
|
||||
* ходу, вызывающему коду ничего дополнительно делать не нужно. Каждая фаза и
|
||||
* итог репортятся отдельным событием по CDC синхронно по ходу прогона (см.
|
||||
* protocol_send_sdram_test_phase()).
|
||||
*/
|
||||
#ifndef DEV_SDRAM_TEST_H_
|
||||
#define DEV_SDRAM_TEST_H_
|
||||
|
||||
/**
|
||||
* @brief Поднять SEMC (bsp_sdram_configure()) и прогнать 4-фазный тест SDRAM.
|
||||
*
|
||||
* Фазы (портированы из firmware/test/src/tests/test_sdram.c без изменений
|
||||
* логики — меняется только то, кто поднимает SEMC до них). Тайминги —
|
||||
* реальный прогон на железе (не оценка из test_sdram.c, та оказалась
|
||||
* консервативнее раз в 6 — запись садится в D-Cache почти мгновенно, реальная
|
||||
* задержка SDRAM только на flush_dcache() и на чтение при верификации):
|
||||
* 1. address_bus — 24 адресных бита (13 row + 9 col + 2 bank), <1 мс.
|
||||
* 2. data_bus — walking ones + инверсия, 64 KB, ~130 мс.
|
||||
* 3. sequential — address pattern + инверсия, 2 MB, ~3.7 с.
|
||||
* 4. retention — запись → flush → 200 мс → verify, 256 KB, ~400 мс.
|
||||
*
|
||||
* Если bsp_sdram_configure()/bsp_sdram_init() не прошли — репортится фаза
|
||||
* "configure" с pass=false, дальнейшие фазы не запускаются.
|
||||
*/
|
||||
void dev_sdram_test_run(void);
|
||||
|
||||
#endif /* DEV_SDRAM_TEST_H_ */
|
||||
113
firmware/bootloader/src/led_status.c
Normal file
113
firmware/bootloader/src/led_status.c
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* @file led_status.c
|
||||
* @brief Реализация словаря LED-паттернов — см. led_status.h и
|
||||
* docs/bootloader/LED_PATTERNS.md.
|
||||
*/
|
||||
|
||||
#include "led_status.h"
|
||||
|
||||
#include "bsp/led.h"
|
||||
#include "bsp/tick.h"
|
||||
#include "bsp/wdog.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ── Периоды паттернов, мс (СИНХРОНИЗИРОВАТЬ с LED_PATTERNS.md) ─────────── */
|
||||
|
||||
/** @brief Heartbeat «жив»: 50 мс горит / 450 мс не горит (период 500). */
|
||||
#define LED_HEARTBEAT_ON_MS 50U
|
||||
#define LED_HEARTBEAT_PERIOD_MS 500U
|
||||
|
||||
/** @brief Неисправность железа: APP 100/100 (период 200). */
|
||||
#define LED_HW_FAULT_ON_MS 100U
|
||||
#define LED_HW_FAULT_PERIOD_MS 200U
|
||||
|
||||
/** @brief Recovery: оба LED синхронно 100/100 (период 200). */
|
||||
#define LED_RECOVERY_ON_MS 100U
|
||||
#define LED_RECOVERY_PERIOD_MS 200U
|
||||
|
||||
/** @brief Установка: APP 250/250 (период 500). */
|
||||
#define LED_INSTALL_ON_MS 250U
|
||||
#define LED_INSTALL_PERIOD_MS 500U
|
||||
|
||||
/** @brief «Образ отклонён»: 4 вспышки по 80 мс вкл / 80 мс выкл. */
|
||||
#define LED_REJECT_ON_MS 80U
|
||||
#define LED_REJECT_COUNT 4U
|
||||
|
||||
/* ── Состояние модуля ──────────────────────────────────────────────────── */
|
||||
|
||||
/** @brief Окно установки открыто — см. led_status_tick_install(). */
|
||||
static bool g_s_installing = false;
|
||||
|
||||
/* ── Внутренние помощники ──────────────────────────────────────────────── */
|
||||
|
||||
/** @brief true, если по текущему тику LED в фазе «горит» для период/on. */
|
||||
static bool phase_on(uint32_t period_ms, uint32_t on_ms)
|
||||
{
|
||||
return (bsp_tick_get_ms() % period_ms) < on_ms;
|
||||
}
|
||||
|
||||
/** @brief Отрисовать системный heartbeat (общий для waiting/hw_fault/install). */
|
||||
static void draw_heartbeat(void)
|
||||
{
|
||||
bsp_led_set(LED_HEARTBEAT, phase_on(LED_HEARTBEAT_PERIOD_MS, LED_HEARTBEAT_ON_MS));
|
||||
}
|
||||
|
||||
/* ── Public API ────────────────────────────────────────────────────────── */
|
||||
|
||||
void led_status_draw_background(led_bg_t bg)
|
||||
{
|
||||
switch (bg)
|
||||
{
|
||||
case LED_BG_RECOVERY:
|
||||
{
|
||||
/* Оба LED — один и тот же фазовый расчёт → строго синхронно. */
|
||||
const bool ON = phase_on(LED_RECOVERY_PERIOD_MS, LED_RECOVERY_ON_MS);
|
||||
bsp_led_set(LED_HEARTBEAT, ON);
|
||||
bsp_led_set(LED_APP, ON);
|
||||
break;
|
||||
}
|
||||
case LED_BG_HW_FAULT:
|
||||
draw_heartbeat(); /* heartbeat в своём ритме 50/450 — не синхронен с APP */
|
||||
bsp_led_set(LED_APP, phase_on(LED_HW_FAULT_PERIOD_MS, LED_HW_FAULT_ON_MS));
|
||||
break;
|
||||
case LED_BG_WAITING:
|
||||
default:
|
||||
draw_heartbeat();
|
||||
bsp_led_off(LED_APP);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void led_status_install_begin(void)
|
||||
{
|
||||
g_s_installing = true;
|
||||
}
|
||||
|
||||
void led_status_install_end(void)
|
||||
{
|
||||
g_s_installing = false;
|
||||
}
|
||||
|
||||
void led_status_tick_install(void)
|
||||
{
|
||||
if (!g_s_installing)
|
||||
{
|
||||
return; /* не установка (revert/recovery-стирание) — ничего не трогаем */
|
||||
}
|
||||
draw_heartbeat();
|
||||
bsp_led_set(LED_APP, phase_on(LED_INSTALL_PERIOD_MS, LED_INSTALL_ON_MS));
|
||||
}
|
||||
|
||||
void led_status_flash_image_rejected(void)
|
||||
{
|
||||
for (uint32_t i = 0U; i < LED_REJECT_COUNT; i++)
|
||||
{
|
||||
bsp_wdog_refresh(); /* ~640 мс блокирующей вспышки — держим watchdog сытым */
|
||||
bsp_led_on(LED_APP);
|
||||
bsp_delay(LED_REJECT_ON_MS);
|
||||
bsp_led_off(LED_APP);
|
||||
bsp_delay(LED_REJECT_ON_MS);
|
||||
}
|
||||
}
|
||||
76
firmware/bootloader/src/led_status.h
Normal file
76
firmware/bootloader/src/led_status.h
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* @file led_status.h
|
||||
* @brief Индикация bootloader на двух LED — единый словарь паттернов.
|
||||
*
|
||||
* Полное человекочитаемое описание (для сервисных инженеров) —
|
||||
* docs/bootloader/LED_PATTERNS.md. Здесь — программный контракт; периоды
|
||||
* в мс заданы в led_status.c и обязаны совпадать с тем документом.
|
||||
*
|
||||
* Разделение на два вида вызова не случайно:
|
||||
* - Фоновые (устойчивые) состояния рисует главный цикл main.c каждую
|
||||
* итерацию: led_status_draw_background().
|
||||
* - Паттерн «идёт установка» рисуется ИЗНУТРИ блокирующей установки (главный
|
||||
* цикл в это время не исполняется): led_status_tick_install(), обёрнутый в
|
||||
* окно led_status_install_begin()/_end(). Вне окна tick — no-op, поэтому
|
||||
* его безопасно звать из flash_area_erase(), который дёргается и на
|
||||
* revert/recovery-стирании, а не только на установке.
|
||||
*/
|
||||
|
||||
#ifndef LED_STATUS_H_
|
||||
#define LED_STATUS_H_
|
||||
|
||||
/** @brief Фоновое (устойчивое) состояние индикации. */
|
||||
typedef enum
|
||||
{
|
||||
LED_BG_WAITING, /**< Норма: HEARTBEAT 50/450, APP выкл — ждём microSD. */
|
||||
LED_BG_HW_FAULT, /**< Неисправность: HEARTBEAT 50/450, APP 100/100. */
|
||||
LED_BG_RECOVERY, /**< Recovery (Фаза 6): оба LED синхронно 100/100. */
|
||||
} led_bg_t;
|
||||
|
||||
/**
|
||||
* @brief Нарисовать фоновый паттерн по текущему тику.
|
||||
*
|
||||
* Вызывать каждую итерацию главного цикла. Приоритет разрешается на стороне
|
||||
* вызывателя (recovery > неисправность > норма) — сюда приходит уже
|
||||
* выбранное состояние.
|
||||
*/
|
||||
void led_status_draw_background(led_bg_t bg);
|
||||
|
||||
/** @brief Открыть окно «идёт установка» — с этого момента tick рисует паттерн. */
|
||||
void led_status_install_begin(void);
|
||||
|
||||
/** @brief Закрыть окно «идёт установка» — tick снова становится no-op. */
|
||||
void led_status_install_end(void);
|
||||
|
||||
/**
|
||||
* @brief Обновить APP/HEARTBEAT под паттерн установки (HEARTBEAT 50/450,
|
||||
* APP 250/250). No-op вне окна install.
|
||||
*
|
||||
* Звать из ВСЕХ блокирующих циклов установки — и поблочного стирания слота
|
||||
* (flash_area_erase, ~5 c на 2 МБ), и копирования чанков (sd_update).
|
||||
*
|
||||
* @note [ИЗВЕСТНОЕ ОГРАНИЧЕНИЕ, подтверждено на железе] Во время самого
|
||||
* стирания блока (~150 мс на 64 КБ, bsp_qspi_erase_block_64k())
|
||||
* qspi_irq_lock() держит __disable_irq() на всю длительность busy-wait
|
||||
* — это глушит и SysTick, на котором держится bsp_tick_get_ms()
|
||||
* (см. bsp/tick/src/tick.c). Часы, от которых считается фаза мигания,
|
||||
* не идут внутри каждого такого окна — тик вызывается исправно между
|
||||
* блоками, но «сейчас» между вызовами почти не меняется, поэтому
|
||||
* глазом видно подвисание/дёрганое мигание, а не плавные 250/250.
|
||||
* Во время копирования чанков (страницы по ~3 мс) окна намного короче
|
||||
* — там мигание заметно более гладкое. Осознанно не чиним (означало бы
|
||||
* не маскировать IRQ на время IP-команды — прямой путь к HardFault,
|
||||
* см. bsp/qspi_flash/README.md, «XIP-безопасность»); фиксируем как
|
||||
* факт в HARDWARE_VERIFICATION_LED_PATTERNS.md.
|
||||
*/
|
||||
void led_status_tick_install(void);
|
||||
|
||||
/**
|
||||
* @brief Разовая индикация «образ с SD отклонён»: APP мигает 4×(80/80).
|
||||
*
|
||||
* Блокирующая (~640 мс), кормит watchdog по ходу. Оставляет APP выключенным —
|
||||
* фоновый паттерн восстановит главный цикл на следующей итерации.
|
||||
*/
|
||||
void led_status_flash_image_rejected(void);
|
||||
|
||||
#endif /* LED_STATUS_H_ */
|
||||
313
firmware/bootloader/src/main.c
Normal file
313
firmware/bootloader/src/main.c
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
/**
|
||||
* @file main.c
|
||||
* @brief bootloader — точка входа.
|
||||
*
|
||||
* Фаза 3: перед выбором образа (bootutil, Direct-XIP) проверяется microSD —
|
||||
* если вставлена, sd_update_check() при необходимости ставит более новый
|
||||
* (или, при удержании BSP_BUTTON_1, принудительно более старый) подписанный
|
||||
* образ в неактивный слот. boot_select_and_jump() вызывается РОВНО ОДИН РАЗ
|
||||
* за попытку — если валидного образа нет, main() возвращается в цикл
|
||||
* ожидания, где SD периодически пере-сканируется (см. sd_update.h о том,
|
||||
* почему boot_go() нельзя звать без новой попытки установки между вызовами).
|
||||
*
|
||||
* Фаза 6 (recovery, см. recovery.h): каждая попытка обёрнута в attempt_boot()
|
||||
* — после SD-скана, но перед прыжком, recovery_decide() решает, обычная ли
|
||||
* это загрузка, нужно ли стереть подозреваемый в зависании слот (счётчик
|
||||
* bsp_boot_attempt_count() дошёл до порога, но есть валидный фолбэк), или
|
||||
* входить в recovery (порог без фолбэка, или удержан BSP_BUTTON_2). Класс A
|
||||
* таксономии (незавершённая установка) закрывается штатным revert MCUboot
|
||||
* без участия этой логики.
|
||||
*
|
||||
* USB CDC поднимается ДО SD-логики (не дожидаясь подключения хоста —
|
||||
* bsp_usb_cdc_write() не блокируется без хоста, см. bsp/usb_cdc/src/usb_cdc.c)
|
||||
* — чтобы статусы ("installing" и т.п.) были видны, если технолог уже
|
||||
* подключён, в т.ч. на самой первой попытке (чек-лист Фазы 3, сценарий 1).
|
||||
*
|
||||
* Последовательность старта:
|
||||
* 1. board_hw_init() — тактирование, MPU, кэш, пины
|
||||
* 2. bsp_wdog_init() — аппаратный watchdog как можно раньше (см. ниже)
|
||||
* 3. bsp_boot_state_init() — POR-детект + счётчик попыток (Фаза 6)
|
||||
* 4. bsp_led_init() — оба LED выключены
|
||||
* 5. bsp_tick_init() — SysTick 1 мс
|
||||
* 6. bsp_button_init() — для проверки удержания BSP_BUTTON_1/2
|
||||
* 7. bsp_qspi_init() — доступ к Slot A/Б
|
||||
* 8. bsp_usb_cdc_init() — не блокирует, см. выше
|
||||
* 9. qspi_info (Фаза 4) — идентификация чипа QSPI (JEDEC → имя +
|
||||
* ёмкость), не зависит от qspi_ok, только CDC
|
||||
* 10. bsp_sdram_configure()+ — smoke-test SDRAM/SEMC (Фаза 4): диагностика,
|
||||
* bsp_sdram_init() не блокирует, результат только на CDC
|
||||
* 11. attempt_boot() — SD-скан + recovery-гейт + прыжок; при
|
||||
* успехе не возвращается
|
||||
* 12. Цикл ожидания — CDC ping/pong + LED + периодический
|
||||
* пере-скан SD (шаг 11 повторно)
|
||||
*
|
||||
* Watchdog (bsp_wdog): единственная защита от бесконечных зависаний в
|
||||
* блокирующих вызовах SDMMC-стека, не возвращающих управление в наш код
|
||||
* (SD_PollingCardInsert / OSA_SemaphoreWait — см. DEBUG_LOG_PHASE3_SD.md).
|
||||
* ⚠️ WDE — write-once: после взвода watchdog не выключить, он переживает прыжок,
|
||||
* поэтому целевой образ (tft_app / test_stub) ОБЯЗАН его кормить (см. bsp/wdog).
|
||||
* Кормим только в точках реального прогресса (верх цикла, циклы стирания/
|
||||
* копирования, перед прыжком) — НЕ перед f_mount/SD_Init, иначе watchdog
|
||||
* перестаёт защищать именно от них.
|
||||
*
|
||||
* Bootloader не зависит от SDRAM (см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md)
|
||||
* — DCD не используется (bsp_boot_xip_no_dcd), XIP только из W25Q. SEMC/SDRAM
|
||||
* трогаются только диагностически, шагом 9 (bsp_sdram_configure(), см.
|
||||
* bsp/sdram/README.md) — bootloader сам эту память ни для чего не использует.
|
||||
*/
|
||||
#include "board.h"
|
||||
#include "boot_select.h"
|
||||
#include "bsp/boot_state.h"
|
||||
#include "bsp/button.h"
|
||||
#include "bsp/led.h"
|
||||
#include "bsp/qspi_flash.h"
|
||||
#include "bsp/sdram.h"
|
||||
#include "bsp/tick.h"
|
||||
#include "bsp/usb_cdc.h"
|
||||
#include "bsp/wdog.h"
|
||||
#include "cli.h"
|
||||
#include "flash_map.h"
|
||||
#include "led_status.h"
|
||||
#include "protocol.h"
|
||||
#include "recovery.h"
|
||||
#include "sd_update.h"
|
||||
#include "slot_version.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ── Одна попытка загрузки: recovery-гейт (Фаза 6) → SD-скан → прыжок ─────
|
||||
*
|
||||
* Порядок важен: recovery_decide() должна знать, входим ли мы в recovery, ДО
|
||||
* SD-скана — от этого зависит, каким gate'ом сканировать SD (обычным строгим
|
||||
* или ослабленным, update_policy_decide(recovery_mode), см. update_policy.h).
|
||||
* Обратный порядок (сначала SD, потом решение) не дал бы recovery-режиму
|
||||
* смысла: строгий gate никогда не поставит образ поверх "активного", даже
|
||||
* если тот активный и есть подозреваемый в зависании слот.
|
||||
*
|
||||
* peek_slot() — та же логика, что private peek_slot() в sd_update.c: не
|
||||
* шарим напрямую между модулями (см. update_policy.h), копия минимальна.
|
||||
* Здесь — только для recovery_decide(); sd_update_check() независимо
|
||||
* повторно пикает слоты внутри себя для update_policy_decide(). */
|
||||
static update_policy_slot_state_t peek_slot(uint8_t fa_id)
|
||||
{
|
||||
update_policy_slot_state_t state;
|
||||
state.valid = slot_version_get(fa_id, &state.version);
|
||||
return state;
|
||||
}
|
||||
|
||||
static void jump_now(void)
|
||||
{
|
||||
bsp_wdog_refresh(); /* образ унаследует полное окно таймаута */
|
||||
boot_select_and_jump(); /* при успехе не возвращается */
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true, если по итогам этой попытки мы (остаёмся) в recovery-режиме
|
||||
* — main() использует это для LED-паттерна/CDC-статуса (Фаза 6b).
|
||||
*/
|
||||
static bool attempt_boot(bool downgrade_held, bool recovery_held)
|
||||
{
|
||||
update_policy_slot_state_t slot_a = peek_slot(0U);
|
||||
update_policy_slot_state_t slot_b = peek_slot(1U);
|
||||
|
||||
recovery_decision_t decision = recovery_decide(
|
||||
bsp_boot_attempt_count(), RECOVERY_DEFAULT_THRESHOLD, &slot_a, &slot_b, recovery_held);
|
||||
|
||||
bool enter_recovery = (decision.action == RECOVERY_ENTER_RECOVERY_MODE);
|
||||
|
||||
bool installed = sd_update_check(downgrade_held, enter_recovery);
|
||||
if (installed)
|
||||
{
|
||||
bsp_boot_attempt_reset(); /* новый образ — новый полный бюджет попыток */
|
||||
}
|
||||
|
||||
if (enter_recovery)
|
||||
{
|
||||
if (installed)
|
||||
{
|
||||
/* Recovery только что поставил валидный образ в Slot A —
|
||||
* прыгаем немедленно, не дожидаясь следующего пере-скана.
|
||||
* Инкремент — как и в обычном пути (см. RECOVERY_NORMAL_BOOT
|
||||
* ниже): первая попытка прыжка в свежий образ тоже расходует
|
||||
* бюджет попыток, симметрично обычной установке. */
|
||||
bsp_boot_attempt_inc();
|
||||
jump_now();
|
||||
}
|
||||
/* Кандидата не нашлось/не прошёл гейт — остаёмся в recovery. */
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (decision.action)
|
||||
{
|
||||
case RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER:
|
||||
{
|
||||
/* Подозреваемый в зависании слот — стереть, есть подтверждённый
|
||||
* фолбэк (recovery_decide() это уже проверила). boot_go() внутри
|
||||
* jump_now() сам выберет оставшийся. */
|
||||
const struct flash_area *p_fap;
|
||||
if (flash_area_open((uint8_t) decision.active_slot, &p_fap) == 0)
|
||||
{
|
||||
(void) flash_area_erase(p_fap, 0U, p_fap->fa_size);
|
||||
flash_area_close(p_fap);
|
||||
}
|
||||
bsp_boot_attempt_reset(); /* ситуация изменилась — новый полный бюджет */
|
||||
jump_now();
|
||||
break;
|
||||
}
|
||||
case RECOVERY_NORMAL_BOOT:
|
||||
default:
|
||||
/* Инкремент только если действительно ЕСТЬ что пытаться загрузить
|
||||
* (слот уже валиден, либо только что установлен этим же вызовом) —
|
||||
* иначе на чисто пустой плате без SD счётчик рос бы и на пустом
|
||||
* месте, и через threshold попыток (несколько секунд) recovery_decide()
|
||||
* ошибочно увела бы в recovery-режим при отсутствии какого-либо
|
||||
* реального зависания (регрессия к сценарию 4 Фазы 3 — "оба слота
|
||||
* пусты" должен оставаться в обычном ожидании SD бесконечно). */
|
||||
if (installed || slot_a.valid || slot_b.valid)
|
||||
{
|
||||
bsp_boot_attempt_inc(); /* перед попыткой — см. bsp/boot_state.h */
|
||||
}
|
||||
jump_now();
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const uint32_t ERROR_BLINK_MS = 250U;
|
||||
const uint32_t SD_RETRY_PERIOD_MS = 1500U;
|
||||
/* Таймаут WDOG. С запасом над самым долгим НАКОРМЛЕННЫМ участком: между
|
||||
* соседними refresh худший легитимный интервал — одиночное стирание 64 КБ
|
||||
* блока (~0.15..2 c по даташиту W25Q) либо цепочка bsp_sd_init+f_mount+пик
|
||||
* слотов (~2-2.5 c). 10 c даёт кратный запас; зависание ловится ≤10 c. */
|
||||
const uint32_t WDOG_TIMEOUT_S = 10U;
|
||||
/* Минимум по docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md §1 — карта
|
||||
* (bootloader+Slot A+Slot Б+запас под ФС ассетов) рассчитана на W25Q128
|
||||
* (16 МБ) и выше; W25Q64 драйвер технически поддерживает, но для этой
|
||||
* платы это неверный BOM, а не "чуть меньше запас". */
|
||||
const uint32_t QSPI_MIN_FLASH_SIZE_MB = 16U;
|
||||
|
||||
board_hw_init();
|
||||
|
||||
/* Как можно раньше — до первой же SD-логики, которая может зависнуть. */
|
||||
(void) bsp_wdog_init(WDOG_TIMEOUT_S);
|
||||
|
||||
/* Сразу после watchdog — сама операция дешёвая (пара регистров SRC), а
|
||||
* решение recovery_decide() ниже нужно уже на первой попытке. */
|
||||
bsp_boot_state_init();
|
||||
|
||||
bsp_led_init();
|
||||
bsp_tick_init();
|
||||
bsp_button_init();
|
||||
|
||||
/* Жест форс. даунгрейда — удержание BSP_BUTTON_1 при подаче питания.
|
||||
* Сэмплируем РОВНО ЗДЕСЬ, до медленной SD-инициализации, и защёлкиваем на
|
||||
* всю сессию: сама установка читает кнопку глубоко внутри run_update()
|
||||
* (после mount + двух крипто-валидаций слотов, секунды спустя), поэтому
|
||||
* читать её там — неинтуитивно (см. DEBUG_LOG_PHASE3_SD.md, тайминг кнопки).
|
||||
* Значение переиспользуется и первой попыткой, и пере-сканами в цикле. */
|
||||
const bool DOWNGRADE_HELD = bsp_button_read(BSP_BUTTON_1);
|
||||
|
||||
/* Жест recovery (Фаза 6) — тот же приём, удержание BSP_BUTTON_2. Приоритет
|
||||
* над BTN_1 разрешается внутри recovery_decide() (проверяется первым). */
|
||||
const bool RECOVERY_HELD = bsp_button_read(BSP_BUTTON_2);
|
||||
|
||||
bool qspi_ok = (bsp_qspi_init() == BSP_OK);
|
||||
bool cdc_ok = (bsp_usb_cdc_init() == BSP_OK);
|
||||
|
||||
/* Единый признак «плата не годна» (LED_BG_HW_FAULT, см. LED_PATTERNS.md) —
|
||||
* накапливается по обоим boot-time чекам ниже (QSPI + SDRAM smoke).
|
||||
* Детали, что именно не так, всегда есть по CDC (qspi_info/smoke_status);
|
||||
* LED показывает лишь факт неисправности. */
|
||||
bool hw_fault = false;
|
||||
|
||||
/* Идентификация QSPI-чипа (Фаза 4) — не зависит от qspi_ok:
|
||||
* bsp_qspi_read_jedec_id() отрабатывает и после проваленного
|
||||
* bsp_qspi_init() (см. её @note), так что "чип не тот"/"чип не опознан"
|
||||
* репортится с деталями, а не просто as "не сработало". */
|
||||
{
|
||||
bsp_qspi_jedec_t jedec = { 0U, 0U };
|
||||
uint32_t qspi_size_mb = 0U;
|
||||
const char *p_qspi_chip = "UNKNOWN";
|
||||
uint8_t qspi_cap_byte = 0U;
|
||||
|
||||
if (bsp_qspi_read_jedec_id(&jedec) == BSP_OK)
|
||||
{
|
||||
qspi_cap_byte = (uint8_t) (jedec.device_id & 0xFFU);
|
||||
p_qspi_chip = bsp_qspi_decode_chip(qspi_cap_byte, &qspi_size_mb);
|
||||
}
|
||||
|
||||
const bool QSPI_PASS = (qspi_size_mb >= QSPI_MIN_FLASH_SIZE_MB);
|
||||
hw_fault = (!qspi_ok) || (!QSPI_PASS); /* чип не отвечает / не тот / мал */
|
||||
protocol_set_qspi_info(jedec.manufacturer_id, p_qspi_chip, qspi_cap_byte, qspi_size_mb, QSPI_PASS);
|
||||
protocol_send_qspi_info(); /* лучший случай — хост уже слушает; см. protocol.h */
|
||||
}
|
||||
|
||||
/* Smoke-test SDRAM/SEMC (Фаза 4) — диагностический, неблокирующий: не
|
||||
* влияет на attempt_boot() ниже (bootloader SDRAM ни для чего не
|
||||
* использует, см. docstring файла), результат только репортится по CDC.
|
||||
* Цель — поймать неисправность SEMC/SDRAM на плате раньше, чем её
|
||||
* унаследует tft_app (см. bsp/sdram/README.md). */
|
||||
bool sdram_ok = (bsp_sdram_configure() == BSP_OK) && (bsp_sdram_init() == BSP_OK);
|
||||
hw_fault = hw_fault || (!sdram_ok);
|
||||
protocol_set_smoke_result(sdram_ok);
|
||||
protocol_send_smoke_status(); /* лучший случай — хост уже слушает; см. protocol.h */
|
||||
|
||||
/* Отслеживает recovery-состояние между попытками — main-loop использует
|
||||
* его для LED-паттерна каждую итерацию, не только на попытках прыжка
|
||||
* (attempt_boot() зовётся раз в SD_RETRY_PERIOD_MS, LED должен обновляться
|
||||
* значительно чаще). */
|
||||
bool in_recovery = false;
|
||||
|
||||
if (qspi_ok)
|
||||
{
|
||||
in_recovery = attempt_boot(DOWNGRADE_HELD, RECOVERY_HELD);
|
||||
}
|
||||
|
||||
/* Нет валидного образа ни в одном слоте (или сбой QSPI) —
|
||||
* диагностический режим. */
|
||||
if (!cdc_ok)
|
||||
{
|
||||
bsp_led_toggle(LED_HEARTBEAT);
|
||||
bsp_delay(ERROR_BLINK_MS);
|
||||
}
|
||||
|
||||
cli_init();
|
||||
|
||||
/* Если предыдущий сброс — по таймауту watchdog, известим (best-effort:
|
||||
* если хост ещё не подключён, сообщение потеряется — состояние всегда
|
||||
* доступно по команде "wdog", см. cli.c). */
|
||||
if (bsp_wdog_caused_last_reset())
|
||||
{
|
||||
protocol_send_wdog_status();
|
||||
}
|
||||
|
||||
/* Готово немедленно — первая попытка сразу извещает "жду SD", не ждёт
|
||||
* SD_RETRY_PERIOD_MS. Дальнейшие попытки уже дросселируются периодом. */
|
||||
uint32_t next_sd_retry_ms = bsp_tick_get_ms();
|
||||
|
||||
while (1)
|
||||
{
|
||||
bsp_wdog_refresh(); /* начало итерации — точка реального прогресса */
|
||||
|
||||
bsp_usb_cdc_poll();
|
||||
cli_process();
|
||||
|
||||
/* Фоновый паттерн: recovery > неисправность железа > норма (ждём SD).
|
||||
* Паттерн «установка» здесь не участвует — он рисуется изнутри самой
|
||||
* (блокирующей) установки, см. led_status_tick_install(). */
|
||||
led_bg_t bg = in_recovery ? LED_BG_RECOVERY : (hw_fault ? LED_BG_HW_FAULT : LED_BG_WAITING);
|
||||
led_status_draw_background(bg);
|
||||
|
||||
if (qspi_ok && ((int32_t) (bsp_tick_get_ms() - next_sd_retry_ms) >= 0))
|
||||
{
|
||||
next_sd_retry_ms = bsp_tick_get_ms() + SD_RETRY_PERIOD_MS;
|
||||
|
||||
protocol_send_status(in_recovery ? "recovery_mode" : "waiting_for_sd");
|
||||
|
||||
in_recovery = attempt_boot(DOWNGRADE_HELD, RECOVERY_HELD);
|
||||
}
|
||||
}
|
||||
}
|
||||
138
firmware/bootloader/src/protocol.c
Normal file
138
firmware/bootloader/src/protocol.c
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
/**
|
||||
* @file protocol.c
|
||||
* @brief Протокол bootloader — реализация сериализации.
|
||||
*/
|
||||
|
||||
#include "protocol.h"
|
||||
|
||||
#include "bsp/boot_state.h"
|
||||
#include "bsp/wdog.h"
|
||||
#include "cli.h"
|
||||
#include "recovery.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef BOOTLOADER_DEV_DIAGNOSTICS
|
||||
#include <inttypes.h>
|
||||
#endif
|
||||
|
||||
/* ── Константы ─────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @brief Размер внутреннего TX-буфера.
|
||||
*
|
||||
* 192, не 128: {"type":"sdram_test",...} с именем фазы "sequential" и
|
||||
* fail_addr/expected/got — самое длинное сообщение протокола, ~140 байт.
|
||||
*/
|
||||
#define PROTO_BUF_SIZE 192U
|
||||
|
||||
/* ── Состояние модуля ──────────────────────────────────────────────────── */
|
||||
|
||||
/** @brief Кэш результата smoke-теста SDRAM/SEMC — см. protocol_send_smoke_status(). */
|
||||
static bool s_smoke_result_known = false;
|
||||
static bool s_smoke_result_pass = false;
|
||||
|
||||
/** @brief Кэш информации о QSPI-чипе — см. protocol_send_qspi_info(). */
|
||||
static bool s_qspi_info_known = false;
|
||||
static bool s_qspi_pass = false;
|
||||
static uint8_t s_qspi_mfr_id = 0U;
|
||||
static uint8_t s_qspi_cap_byte = 0U;
|
||||
static uint32_t s_qspi_size_mb = 0U;
|
||||
static const char *s_p_qspi_chip = ""; /* строковый литерал из bsp_qspi_decode_chip() — статичен */
|
||||
|
||||
/* ── Public API ────────────────────────────────────────────────────────── */
|
||||
|
||||
void protocol_send_pong(void)
|
||||
{
|
||||
cli_send("{\"type\":\"pong\"}\n");
|
||||
}
|
||||
|
||||
void protocol_send_version_response(void)
|
||||
{
|
||||
char buf[PROTO_BUF_SIZE];
|
||||
(void) snprintf(buf, sizeof(buf),
|
||||
"{\"type\":\"version_response\","
|
||||
"\"fw\":\"" BOOTLOADER_VERSION "\"}\n");
|
||||
cli_send(buf);
|
||||
}
|
||||
|
||||
void protocol_send_error(const char *p_code)
|
||||
{
|
||||
char buf[PROTO_BUF_SIZE];
|
||||
(void) snprintf(buf, sizeof(buf), "{\"ok\":false,\"error\":\"%s\"}\n", p_code);
|
||||
cli_send(buf);
|
||||
}
|
||||
|
||||
void protocol_send_status(const char *p_state)
|
||||
{
|
||||
char buf[PROTO_BUF_SIZE];
|
||||
(void) snprintf(buf, sizeof(buf), "{\"type\":\"status\",\"state\":\"%s\"}\n", p_state);
|
||||
cli_send(buf);
|
||||
}
|
||||
|
||||
void protocol_set_smoke_result(bool pass)
|
||||
{
|
||||
s_smoke_result_known = true;
|
||||
s_smoke_result_pass = pass;
|
||||
}
|
||||
|
||||
void protocol_send_smoke_status(void)
|
||||
{
|
||||
if (s_smoke_result_known)
|
||||
{
|
||||
protocol_send_status(s_smoke_result_pass ? "smoke_pass" : "smoke_fail");
|
||||
}
|
||||
}
|
||||
|
||||
void protocol_set_qspi_info(uint8_t mfr_id, const char *p_chip_name, uint8_t cap_byte, uint32_t size_mb, bool pass)
|
||||
{
|
||||
s_qspi_info_known = true;
|
||||
s_qspi_mfr_id = mfr_id;
|
||||
s_p_qspi_chip = p_chip_name;
|
||||
s_qspi_cap_byte = cap_byte;
|
||||
s_qspi_size_mb = size_mb;
|
||||
s_qspi_pass = pass;
|
||||
}
|
||||
|
||||
void protocol_send_qspi_info(void)
|
||||
{
|
||||
if (!s_qspi_info_known)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
char buf[PROTO_BUF_SIZE];
|
||||
(void) snprintf(buf, sizeof(buf),
|
||||
"{\"type\":\"qspi_info\",\"chip\":\"%s\",\"mfr\":\"0x%02X\","
|
||||
"\"cap_byte\":\"0x%02X\",\"size_mb\":%u,\"pass\":%s}\n",
|
||||
s_p_qspi_chip, (unsigned) s_qspi_mfr_id, (unsigned) s_qspi_cap_byte,
|
||||
(unsigned) s_qspi_size_mb, s_qspi_pass ? "true" : "false");
|
||||
cli_send(buf);
|
||||
}
|
||||
|
||||
void protocol_send_wdog_status(void)
|
||||
{
|
||||
char buf[PROTO_BUF_SIZE];
|
||||
(void) snprintf(buf, sizeof(buf),
|
||||
"{\"type\":\"wdog\",\"armed\":%s,\"timeout_s\":%u,\"recovered\":%s,"
|
||||
"\"reset_count\":%u,\"threshold\":%u}\n",
|
||||
bsp_wdog_is_armed() ? "true" : "false",
|
||||
(unsigned) bsp_wdog_timeout_s(),
|
||||
bsp_wdog_caused_last_reset() ? "true" : "false",
|
||||
(unsigned) bsp_boot_attempt_count(),
|
||||
(unsigned) RECOVERY_DEFAULT_THRESHOLD);
|
||||
cli_send(buf);
|
||||
}
|
||||
|
||||
#ifdef BOOTLOADER_DEV_DIAGNOSTICS
|
||||
void protocol_send_sdram_test_phase(
|
||||
const char *p_phase, bool pass, uint32_t duration_ms, uint32_t fail_addr, uint8_t expected, uint8_t got)
|
||||
{
|
||||
char buf[PROTO_BUF_SIZE];
|
||||
(void) snprintf(buf, sizeof(buf),
|
||||
"{\"type\":\"sdram_test\",\"phase\":\"%s\",\"pass\":%s,\"duration_ms\":%" PRIu32
|
||||
",\"fail_addr\":\"0x%08" PRIX32 "\",\"expected\":\"0x%02X\",\"got\":\"0x%02X\"}\n",
|
||||
p_phase, pass ? "true" : "false", duration_ms, fail_addr, (unsigned) expected, (unsigned) got);
|
||||
cli_send(buf);
|
||||
}
|
||||
#endif /* BOOTLOADER_DEV_DIAGNOSTICS */
|
||||
177
firmware/bootloader/src/protocol.h
Normal file
177
firmware/bootloader/src/protocol.h
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
/**
|
||||
* @file protocol.h
|
||||
* @brief Протокол bootloader — сериализация исходящих событий.
|
||||
*
|
||||
* Урезанное подмножество протокола firmware_test (firmware/test/src/protocol.h):
|
||||
* только то, что нужно для диагностики bootloader через service-tui. Без
|
||||
* test_begin/test_result/confirm_request/session_start — те специфичны для
|
||||
* тестового раннера firmware_test.
|
||||
*
|
||||
* Все функции формируют JSON-строку и отправляют через cli_send().
|
||||
* Без динамической памяти — каждая функция пишет в стековый буфер.
|
||||
*
|
||||
* Типы исходящих событий (Фаза 1):
|
||||
* pong — ответ на {"type":"cmd","cmd":"ping"}
|
||||
* version_response — ответ на {"type":"cmd","cmd":"get_version"}
|
||||
* error — ошибка протокола или парсинга
|
||||
*
|
||||
* Типы исходящих событий (Фаза 3):
|
||||
* status — top-level состояние bootloader (waiting_for_sd,
|
||||
* installing, update_skipped, recovery_mode,
|
||||
* smoke_pass/smoke_fail (Фаза 4, SDRAM/SEMC smoke-test,
|
||||
* см. main.c) — см. protocol_send_status())
|
||||
* wdog — статус аппаратного watchdog (armed/timeout/recovered,
|
||||
* см. protocol_send_wdog_status())
|
||||
* qspi_info — опознанный чип QSPI flash + вписывается ли в минимум
|
||||
* карты флеша (Фаза 4, см. protocol_send_qspi_info())
|
||||
*
|
||||
* smoke_pass/smoke_fail и qspi_info шлются один раз сразу после
|
||||
* bsp_usb_cdc_init()/bsp_qspi_init() — хост почти наверняка не успевает
|
||||
* открыть порт к этому моменту (USB enumeration), cli_send() неблокирующий и
|
||||
* теряет запись, если TX ещё не готов. Решение для обоих одинаковое:
|
||||
* protocol_set_smoke_result()/protocol_set_qspi_info() кэширует исход,
|
||||
* protocol_send_smoke_status()/protocol_send_qspi_info() переспрашивает его в
|
||||
* любой момент сессии по командам "smoke_status"/"qspi_info" (тот же приём,
|
||||
* что уже был у wdog).
|
||||
*
|
||||
* Ещё не реализовано (Фаза 4): состояние "booting" и словарь LED-паттернов
|
||||
* на LED_APP для всех перечисленных состояний — см. firmware/bootloader/PLAN.md.
|
||||
*
|
||||
* [DEV-ONLY, Фаза 4] Тип sdram_test — под BOOTLOADER_DEV_DIAGNOSTICS
|
||||
* (компилируется только в Debug, см. CMakeLists.txt и dev_sdram_test.h):
|
||||
* sdram_test — результат одной фазы глубокого теста SDRAM, см.
|
||||
* protocol_send_sdram_test_phase() и dev_sdram_test.h
|
||||
*/
|
||||
|
||||
#ifndef PROTOCOL_H_
|
||||
#define PROTOCOL_H_
|
||||
|
||||
#include "version.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/** @brief Строка версии bootloader, вставляемая в version_response. */
|
||||
#define BOOTLOADER_VERSION BOOTLOADER_VERSION_STR
|
||||
|
||||
/**
|
||||
* @brief Отправить pong — ответ на ping.
|
||||
*/
|
||||
void protocol_send_pong(void);
|
||||
|
||||
/**
|
||||
* @brief Отправить ответ на команду get_version.
|
||||
*
|
||||
* Формат: {"type":"version_response","fw":"X.Y.Z"}
|
||||
*/
|
||||
void protocol_send_version_response(void);
|
||||
|
||||
/**
|
||||
* @brief Отправить событие error.
|
||||
*
|
||||
* @param[in] p_code Короткий ASCII-код ошибки, напр. "PARSE_ERR".
|
||||
*/
|
||||
void protocol_send_error(const char *p_code);
|
||||
|
||||
/**
|
||||
* @brief Отправить событие status — top-level состояние bootloader.
|
||||
*
|
||||
* Формат: {"type":"status","state":"waiting_for_sd"}
|
||||
*
|
||||
* @param[in] p_state Короткий ASCII-идентификатор состояния,
|
||||
* напр. "waiting_for_sd", "installing".
|
||||
*/
|
||||
void protocol_send_status(const char *p_state);
|
||||
|
||||
/**
|
||||
* @brief Сохранить результат smoke-теста SDRAM/SEMC (Фаза 4) для последующих
|
||||
* protocol_send_smoke_status().
|
||||
*
|
||||
* Вызывать один раз из main.c сразу после bsp_sdram_configure()+_init().
|
||||
*
|
||||
* @param[in] pass true, если оба вызова вернули BSP_OK.
|
||||
*/
|
||||
void protocol_set_smoke_result(bool pass);
|
||||
|
||||
/**
|
||||
* @brief Отправить status с последним сохранённым результатом smoke-теста.
|
||||
*
|
||||
* Формат: {"type":"status","state":"smoke_pass"} / "smoke_fail". Ничего не
|
||||
* делает, если protocol_set_smoke_result() ещё ни разу не вызывался (не
|
||||
* должно происходить в штатной последовательности main.c, но на команду
|
||||
* "smoke_status" в этом случае лучше промолчать, чем соврать результат).
|
||||
*/
|
||||
void protocol_send_smoke_status(void);
|
||||
|
||||
/**
|
||||
* @brief Сохранить информацию об обнаруженном QSPI flash-чипе (Фаза 4) для
|
||||
* последующих protocol_send_qspi_info().
|
||||
*
|
||||
* Вызывать один раз из main.c сразу после bsp_qspi_init(). Значения
|
||||
* читаются напрямую по JEDEC ID (bsp_qspi_decode_chip()), не зависят от
|
||||
* успеха bsp_qspi_init() — так неопознанный/не тот чип тоже репортится с
|
||||
* деталями, а не просто "не сработало".
|
||||
*
|
||||
* @param[in] mfr_id Сырой manufacturer byte JEDEC ID.
|
||||
* @param[in] p_chip_name Имя чипа ("W25Q128", "UNKNOWN") — см. bsp_qspi_decode_chip().
|
||||
* @param[in] cap_byte Сырой capacity byte JEDEC ID.
|
||||
* @param[in] size_mb Обнаруженная ёмкость, МБ (0, если чип не опознан).
|
||||
* @param[in] pass true, если Winbond + известная ёмкость + ёмкость не
|
||||
* меньше минимума карты флеша (см. main.c).
|
||||
*/
|
||||
void protocol_set_qspi_info(
|
||||
uint8_t mfr_id, const char *p_chip_name, uint8_t cap_byte, uint32_t size_mb, bool pass);
|
||||
|
||||
/**
|
||||
* @brief Отправить event с последней сохранённой информацией о QSPI-чипе.
|
||||
*
|
||||
* Формат: {"type":"qspi_info","chip":"W25Q128","mfr":"0xEF","cap_byte":"0x18",
|
||||
* "size_mb":16,"pass":true}
|
||||
*
|
||||
* Ничего не делает, если protocol_set_qspi_info() ещё не вызывался.
|
||||
*/
|
||||
void protocol_send_qspi_info(void);
|
||||
|
||||
/**
|
||||
* @brief Отправить статус аппаратного watchdog и счётчика попыток загрузки
|
||||
* (Фаза 6).
|
||||
*
|
||||
* Формат: {"type":"wdog","armed":true,"timeout_s":10,"recovered":false,
|
||||
* "reset_count":0,"threshold":3}
|
||||
* - armed — watchdog взведён (bsp_wdog_init выполнен);
|
||||
* - timeout_s — сконфигурированный таймаут в секундах;
|
||||
* - recovered — ПОСЛЕДНИЙ сброс МК был по таймауту watchdog (плата
|
||||
* восстановилась после зависания);
|
||||
* - reset_count — bsp_boot_attempt_count(): сколько попыток подряд без
|
||||
* подтверждения здоровья (health-mark/новая установка), 0
|
||||
* сразу после POR;
|
||||
* - threshold — RECOVERY_DEFAULT_THRESHOLD: порог фолбэка/recovery.
|
||||
*
|
||||
* Эмитится один раз на старте, если recovered, и по команде "wdog".
|
||||
*/
|
||||
void protocol_send_wdog_status(void);
|
||||
|
||||
#ifdef BOOTLOADER_DEV_DIAGNOSTICS
|
||||
/**
|
||||
* @brief [DEV-ONLY] Отправить результат одной фазы dev_sdram_test (Фаза 4).
|
||||
*
|
||||
* Формат: {"type":"sdram_test","phase":"data_bus","pass":true,"duration_ms":812,
|
||||
* "fail_addr":"0x00000000","expected":"0x00","got":"0x00"}
|
||||
*
|
||||
* fail_addr/expected/got осмысленны только при pass=false — при pass=true
|
||||
* передавать 0/0/0. phase="summary" — итог всего прогона (все фазы пройдены
|
||||
* И до summary дошло — см. dev_sdram_test.c).
|
||||
*
|
||||
* @param[in] p_phase "configure"/"address_bus"/"data_bus"/"sequential"/
|
||||
* "retention"/"summary".
|
||||
* @param[in] pass Результат фазы.
|
||||
* @param[in] duration_ms Длительность фазы, мс.
|
||||
* @param[in] fail_addr Адрес первой ошибки (0, если pass=true).
|
||||
* @param[in] expected Ожидаемый байт при ошибке.
|
||||
* @param[in] got Прочитанный байт при ошибке.
|
||||
*/
|
||||
void protocol_send_sdram_test_phase(
|
||||
const char *p_phase, bool pass, uint32_t duration_ms, uint32_t fail_addr, uint8_t expected, uint8_t got);
|
||||
#endif /* BOOTLOADER_DEV_DIAGNOSTICS */
|
||||
|
||||
#endif /* PROTOCOL_H_ */
|
||||
75
firmware/bootloader/src/recovery.c
Normal file
75
firmware/bootloader/src/recovery.c
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* @file recovery.c
|
||||
* @brief Реализация — см. recovery.h.
|
||||
*/
|
||||
|
||||
#include "recovery.h"
|
||||
|
||||
/* ── Определение активного слота ──────────────────────────────────────────
|
||||
* Тот же приём, что find_active_slot() в update_policy.c (private там) —
|
||||
* не шарим напрямую, копия минимальна и независимо тестируется здесь же,
|
||||
* как уже принято в проекте для мелкой логики между модулями. */
|
||||
|
||||
typedef struct
|
||||
{
|
||||
bool have_active;
|
||||
update_policy_slot_t active_slot;
|
||||
struct image_version active_ver;
|
||||
} active_slot_info_t;
|
||||
|
||||
static active_slot_info_t find_active_slot(const update_policy_slot_state_t *p_slot_a,
|
||||
const update_policy_slot_state_t *p_slot_b)
|
||||
{
|
||||
active_slot_info_t info = { .have_active = false };
|
||||
|
||||
if (p_slot_a->valid)
|
||||
{
|
||||
info.have_active = true;
|
||||
info.active_slot = UPDATE_POLICY_SLOT_A;
|
||||
info.active_ver = p_slot_a->version;
|
||||
}
|
||||
|
||||
if (p_slot_b->valid &&
|
||||
(!info.have_active || image_version_compare(&p_slot_b->version, &info.active_ver) > 0))
|
||||
{
|
||||
info.have_active = true;
|
||||
info.active_slot = UPDATE_POLICY_SLOT_B;
|
||||
info.active_ver = p_slot_b->version;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
static bool other_slot_valid(update_policy_slot_t active_slot,
|
||||
const update_policy_slot_state_t *p_slot_a,
|
||||
const update_policy_slot_state_t *p_slot_b)
|
||||
{
|
||||
return (active_slot == UPDATE_POLICY_SLOT_A) ? p_slot_b->valid : p_slot_a->valid;
|
||||
}
|
||||
|
||||
/* ── Публичный API ─────────────────────────────────────────────────────── */
|
||||
|
||||
recovery_decision_t recovery_decide(uint32_t attempt_count, uint32_t threshold,
|
||||
const update_policy_slot_state_t *p_slot_a,
|
||||
const update_policy_slot_state_t *p_slot_b, bool btn2_held)
|
||||
{
|
||||
if (btn2_held)
|
||||
{
|
||||
return (recovery_decision_t) { .action = RECOVERY_ENTER_RECOVERY_MODE };
|
||||
}
|
||||
|
||||
if (attempt_count < threshold)
|
||||
{
|
||||
return (recovery_decision_t) { .action = RECOVERY_NORMAL_BOOT };
|
||||
}
|
||||
|
||||
active_slot_info_t active = find_active_slot(p_slot_a, p_slot_b);
|
||||
|
||||
if (active.have_active && other_slot_valid(active.active_slot, p_slot_a, p_slot_b))
|
||||
{
|
||||
return (recovery_decision_t) { .action = RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER,
|
||||
.active_slot = active.active_slot };
|
||||
}
|
||||
|
||||
return (recovery_decision_t) { .action = RECOVERY_ENTER_RECOVERY_MODE };
|
||||
}
|
||||
72
firmware/bootloader/src/recovery.h
Normal file
72
firmware/bootloader/src/recovery.h
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* @file recovery.h
|
||||
* @brief Чистая логика решения "что делать с этой попыткой загрузки" —
|
||||
* без аппаратных зависимостей (flash/SRC_GPR), полностью
|
||||
* host-тестируема.
|
||||
*
|
||||
* См. firmware/bootloader/PLAN.md, Фаза 6, разбивка 6a.
|
||||
*/
|
||||
|
||||
#ifndef RECOVERY_H_
|
||||
#define RECOVERY_H_
|
||||
|
||||
#include "update_policy.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum
|
||||
{
|
||||
RECOVERY_NORMAL_BOOT = 0,
|
||||
RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER,
|
||||
RECOVERY_ENTER_RECOVERY_MODE,
|
||||
} recovery_action_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
recovery_action_t action;
|
||||
|
||||
/**
|
||||
* Слот, который нужно стереть перед прыжком. Значим только при
|
||||
* action == RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER.
|
||||
*/
|
||||
update_policy_slot_t active_slot;
|
||||
} recovery_decision_t;
|
||||
|
||||
/**
|
||||
* @brief Решить, что делать с текущей попыткой загрузки — таксономия
|
||||
* отказов Фазы 6 (классы B/C/D; класс A закрывается штатным revert
|
||||
* MCUboot без участия этой функции).
|
||||
*
|
||||
* Правила (приоритет сверху вниз):
|
||||
* - btn2_held → RECOVERY_ENTER_RECOVERY_MODE. Ручной триггер главнее
|
||||
* счётчика — оператор может войти в recovery в любой момент, независимо
|
||||
* от истории попыток.
|
||||
* - attempt_count < threshold → RECOVERY_NORMAL_BOOT. Обычная загрузка,
|
||||
* ничего не предпринимаем (счётчик инкрементирует вызывающий код перед
|
||||
* прыжком).
|
||||
* - attempt_count >= threshold и есть "активный" слот (валидный, более
|
||||
* высокой версии — тот же приём, что update_policy_decide()) — он и
|
||||
* есть подозреваемый в зависании:
|
||||
* - другой слот валиден (есть фолбэк) → RECOVERY_ERASE_ACTIVE_THEN_
|
||||
* BOOT_OTHER(active_slot). Вызывающий код стирает active_slot и
|
||||
* обнуляет счётчик — boot_go() сам выберет оставшийся слот.
|
||||
* - другого валидного слота нет (стирать нечего — иначе ноль рабочих
|
||||
* слотов) → RECOVERY_ENTER_RECOVERY_MODE.
|
||||
* - attempt_count >= threshold и активного слота нет вообще →
|
||||
* RECOVERY_ENTER_RECOVERY_MODE (нечего анализировать, нечего стирать).
|
||||
*
|
||||
* @param[in] attempt_count Текущее значение счётчика попыток (bsp_boot_attempt_count()).
|
||||
* @param[in] threshold Порог срабатывания фолбэка/recovery (см. RECOVERY_DEFAULT_THRESHOLD).
|
||||
* @param[in] p_slot_a Состояние Slot A.
|
||||
* @param[in] p_slot_b Состояние Slot Б.
|
||||
* @param[in] btn2_held BSP_BUTTON_2 удержана на старте — ручной вход в recovery.
|
||||
*/
|
||||
recovery_decision_t recovery_decide(uint32_t attempt_count, uint32_t threshold,
|
||||
const update_policy_slot_state_t *p_slot_a,
|
||||
const update_policy_slot_state_t *p_slot_b, bool btn2_held);
|
||||
|
||||
/** @brief Дефолтный порог — 3 сброса подряд до фолбэка/recovery. */
|
||||
#define RECOVERY_DEFAULT_THRESHOLD 3U
|
||||
|
||||
#endif /* RECOVERY_H_ */
|
||||
273
firmware/bootloader/src/sd_update.c
Normal file
273
firmware/bootloader/src/sd_update.c
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
/**
|
||||
* @file sd_update.c
|
||||
* @brief Реализация — см. sd_update.h.
|
||||
*
|
||||
* Гейт принятия кандидата — двухступенчатый:
|
||||
* 1. Лёгкий пик заголовка (magic + версия) сразу с SD, до касания flash —
|
||||
* достаточно для решения install/skip (update_policy_decide()).
|
||||
* 2. Полная криптографическая проверка (hash+ECDSA) — уже ПОСЛЕ записи в
|
||||
* целевой слот, переиспользованием slot_version_get() (та же функция,
|
||||
* что и для пика активного слота). Если кандидат подписан неверно —
|
||||
* slot_version_get() на целевом слоте вернёт false, а последующий
|
||||
* (единственный) boot_go() в main.c просто не выберет этот слот и
|
||||
* останется на прежнем валидном — отдельный shim "flash_area поверх SD-
|
||||
* файла" не нужен.
|
||||
*
|
||||
* Целевой слот при установке — всегда НЕ активный (см. update_policy.h):
|
||||
* уже выбранный на этот момент слот этой функцией никогда не стирается.
|
||||
* Исключение — recovery-режим (Фаза 6b, update_policy_decide(recovery_mode)):
|
||||
* там целевой слот всегда Slot A, независимо от того, что было активно.
|
||||
*/
|
||||
|
||||
#include "sd_update.h"
|
||||
|
||||
#include "bootutil/image.h"
|
||||
#include "bsp/sd.h"
|
||||
#include "bsp/usb_cdc.h"
|
||||
#include "bsp/wdog.h"
|
||||
#include "ff.h"
|
||||
#include "flash_map.h"
|
||||
#include "led_status.h"
|
||||
#include "protocol.h"
|
||||
#include "slot_version.h"
|
||||
#include "update_policy.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* ── Константы ─────────────────────────────────────────────────────────── */
|
||||
|
||||
#define SD_UPDATE_MOUNT_POINT "2:/"
|
||||
#define SD_UPDATE_FILE_PATH "2:/TFT_APP.BIN"
|
||||
|
||||
/** @brief Размер чанка потокового копирования SD → flash. */
|
||||
#define SD_UPDATE_CHUNK_SIZE 4096U
|
||||
|
||||
/* ── Состояние модуля ────────────────────────────────────────────────────── */
|
||||
|
||||
static FATFS g_s_fs;
|
||||
static FIL g_s_file;
|
||||
static uint8_t g_s_chunk_buf[SD_UPDATE_CHUNK_SIZE];
|
||||
static uint8_t g_s_verify_buf[SD_UPDATE_CHUNK_SIZE];
|
||||
|
||||
/* ── Вспомогательные функции ───────────────────────────────────────────── */
|
||||
|
||||
static update_policy_slot_state_t peek_slot(uint8_t fa_id)
|
||||
{
|
||||
update_policy_slot_state_t state;
|
||||
state.valid = slot_version_get(fa_id, &state.version);
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Прочитать заголовок кандидата с начала уже открытого файла.
|
||||
* @retval true magic верный — версия в *p_out_ver, позиция файла = sizeof(header).
|
||||
* @retval false Ошибка чтения либо неверный magic.
|
||||
*/
|
||||
static bool read_candidate_header(struct image_version *p_out_ver)
|
||||
{
|
||||
struct image_header hdr;
|
||||
UINT br = 0U;
|
||||
|
||||
FRESULT fr = f_read(&g_s_file, &hdr, sizeof(hdr), &br);
|
||||
if ((fr != FR_OK) || (br != sizeof(hdr)) || (hdr.ih_magic != IMAGE_MAGIC))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*p_out_ver = hdr.ih_ver;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Стереть целевой слот и потоково скопировать в него файл-кандидат
|
||||
* (с начала файла — заголовок читается заново), сверяя каждый
|
||||
* записанный чанк немедленным обратным чтением.
|
||||
*
|
||||
* @return true при успехе (весь файл скопирован и каждый чанк совпал).
|
||||
*/
|
||||
static bool erase_and_copy_candidate(const struct flash_area *p_fap, uint32_t file_size)
|
||||
{
|
||||
if (file_size > p_fap->fa_size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (flash_area_erase(p_fap, 0U, p_fap->fa_size) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (f_lseek(&g_s_file, 0) != FR_OK)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t offset = 0U;
|
||||
while (offset < file_size)
|
||||
{
|
||||
bsp_usb_cdc_poll();
|
||||
bsp_wdog_refresh(); /* потоковое копирование — реальный прогресс на чанк */
|
||||
led_status_tick_install(); /* APP 250/250 всю установку, см. led_status.h */
|
||||
|
||||
uint32_t want = file_size - offset;
|
||||
if (want > SD_UPDATE_CHUNK_SIZE)
|
||||
{
|
||||
want = SD_UPDATE_CHUNK_SIZE;
|
||||
}
|
||||
|
||||
UINT br = 0U;
|
||||
if ((f_read(&g_s_file, g_s_chunk_buf, want, &br) != FR_OK) || (br != want))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (flash_area_write(p_fap, offset, g_s_chunk_buf, want) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((flash_area_read(p_fap, offset, g_s_verify_buf, want) != 0) ||
|
||||
(memcmp(g_s_chunk_buf, g_s_verify_buf, want) != 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset += want;
|
||||
}
|
||||
|
||||
bsp_usb_cdc_poll();
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── Основной сценарий ────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @return true, если target_slot после этого вызова содержит новый,
|
||||
* подтверждённый (slot_version_get()) образ — см. sd_update.h.
|
||||
*/
|
||||
static bool run_update(bool button_held, bool recovery_mode)
|
||||
{
|
||||
struct image_version candidate_ver;
|
||||
struct image_version installed_ver;
|
||||
update_policy_slot_state_t slot_a;
|
||||
update_policy_slot_state_t slot_b;
|
||||
update_policy_result_t decision;
|
||||
const struct flash_area *p_fap = NULL;
|
||||
uint32_t file_size;
|
||||
bool copy_ok;
|
||||
bool result = false;
|
||||
|
||||
if (bsp_sd_init() != BSP_OK)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (f_mount(&g_s_fs, SD_UPDATE_MOUNT_POINT, 1) != FR_OK)
|
||||
{
|
||||
(void) bsp_sd_deinit();
|
||||
return false; /* нет карты/файловой системы — штатно, не ошибка */
|
||||
}
|
||||
|
||||
if (f_open(&g_s_file, SD_UPDATE_FILE_PATH, FA_READ) != FR_OK)
|
||||
{
|
||||
(void) f_unmount(SD_UPDATE_MOUNT_POINT);
|
||||
(void) bsp_sd_deinit();
|
||||
return false; /* TFT_APP.BIN отсутствует — тоже штатно */
|
||||
}
|
||||
|
||||
if (!read_candidate_header(&candidate_ver))
|
||||
{
|
||||
protocol_send_error("SD_CANDIDATE_INVALID");
|
||||
led_status_flash_image_rejected(); /* битый заголовок = негодный файл */
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
file_size = (uint32_t) f_size(&g_s_file);
|
||||
slot_a = peek_slot(0U);
|
||||
slot_b = peek_slot(1U);
|
||||
/* button_held — сэмплирован при старте в main.c и передан сюда (см.
|
||||
* sd_update.h). recovery_mode — ослабленный gate Фазы 6b, см.
|
||||
* update_policy.h; решение "входить ли в recovery" — не здесь. */
|
||||
decision = update_policy_decide(&slot_a, &slot_b, &candidate_ver, button_held, recovery_mode);
|
||||
|
||||
if (decision.action == UPDATE_POLICY_SKIP)
|
||||
{
|
||||
protocol_send_status("update_skipped");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
protocol_send_status("installing");
|
||||
led_status_install_begin(); /* открыть окно: tick_install() ниже начинает рисовать */
|
||||
|
||||
if (flash_area_open((uint8_t) decision.target_slot, &p_fap) != 0)
|
||||
{
|
||||
protocol_send_error("SD_INSTALL_WRITE_FAILED");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
copy_ok = erase_and_copy_candidate(p_fap, file_size);
|
||||
flash_area_close(p_fap);
|
||||
|
||||
if (!copy_ok)
|
||||
{
|
||||
protocol_send_error("SD_INSTALL_WRITE_FAILED");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (!slot_version_get((uint8_t) decision.target_slot, &installed_ver))
|
||||
{
|
||||
protocol_send_error("SD_INSTALL_REJECTED");
|
||||
led_status_flash_image_rejected(); /* записан, но подпись не прошла = негодный файл */
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* target_slot подтверждён валидным — установка состоялась независимо от
|
||||
* исхода стирания "второго" слота ниже (оно диагностируется отдельным
|
||||
* protocol_send_error, но не отменяет уже подтверждённый результат). */
|
||||
result = true;
|
||||
|
||||
/* Форс. даунгрейд и recovery (см. update_policy.h): без стирания
|
||||
* прежнего активного/Slot Б он остался бы валиден (и новее в обычном
|
||||
* режиме) и снова выиграл бы в boot_go() — даунгрейд/recovery физически
|
||||
* записались бы, но не загрузились. Стираем ТОЛЬКО теперь, когда новый
|
||||
* образ уже подтверждён валидным — на диске никогда не бывает нуля
|
||||
* рабочих слотов. */
|
||||
if (decision.erase_previous_active)
|
||||
{
|
||||
const struct flash_area *p_peer_fap;
|
||||
uint8_t peer_slot = (decision.target_slot == UPDATE_POLICY_SLOT_A) ? UPDATE_POLICY_SLOT_B
|
||||
: UPDATE_POLICY_SLOT_A;
|
||||
|
||||
if (flash_area_open(peer_slot, &p_peer_fap) != 0)
|
||||
{
|
||||
protocol_send_error("SD_DOWNGRADE_ERASE_FAILED");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (flash_area_erase(p_peer_fap, 0U, p_peer_fap->fa_size) != 0)
|
||||
{
|
||||
protocol_send_error("SD_DOWNGRADE_ERASE_FAILED");
|
||||
}
|
||||
flash_area_close(p_peer_fap);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup:
|
||||
led_status_install_end(); /* закрыть окно (идемпотентно, если не открывали) */
|
||||
(void) f_close(&g_s_file);
|
||||
(void) f_unmount(SD_UPDATE_MOUNT_POINT);
|
||||
(void) bsp_sd_deinit();
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ── Public API ────────────────────────────────────────────────────────── */
|
||||
|
||||
bool sd_update_check(bool downgrade_button_held, bool recovery_mode)
|
||||
{
|
||||
if (!bsp_sd_is_inserted())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return run_update(downgrade_button_held, recovery_mode);
|
||||
}
|
||||
50
firmware/bootloader/src/sd_update.h
Normal file
50
firmware/bootloader/src/sd_update.h
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* @file sd_update.h
|
||||
* @brief Оркестрация установки образа tft_app с microSD в неактивный слот.
|
||||
*
|
||||
* См. firmware/bootloader/PLAN.md, Фаза 3.
|
||||
*/
|
||||
|
||||
#ifndef SD_UPDATE_H_
|
||||
#define SD_UPDATE_H_
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* @brief Одна попытка: смонтировать SD, найти TFT_APP.BIN, при необходимости
|
||||
* установить его в целевой слот (см. update_policy.h).
|
||||
*
|
||||
* Обычный режим (recovery_mode == false) пишет только в неактивный слот —
|
||||
* уже выбранный/загружаемый слот никогда не трогается. Recovery-режим
|
||||
* (Фаза 6b) — ослабленный gate: любой подписанный кандидат ставится в Slot A
|
||||
* безусловно, Slot Б стирается. Сам не вызывает boot_go()/
|
||||
* boot_select_and_jump() — решение "когда прыгать" остаётся за main.c,
|
||||
* которое обязано вызвать его ровно один раз за сессию питания (см.
|
||||
* slot_version.h о том, почему boot_go() нельзя звать повторно).
|
||||
*
|
||||
* Ничего не делает, если SD не вставлена (bsp_sd_is_inserted() == false) —
|
||||
* безопасно вызывать многократно, в т.ч. из цикла ожидания в main.c.
|
||||
*
|
||||
* @param downgrade_button_held Состояние BSP_BUTTON_1, сэмплированное ОДИН РАЗ
|
||||
* при старте (main.c, до медленной SD-инициализации) и защёлкнутое.
|
||||
* true → разрешён форс. даунгрейд более старого подписанного образа
|
||||
* (см. update_policy.h). Передаётся, а не читается здесь, чтобы жест
|
||||
* "удержание при включении" ловился в предсказуемый ранний момент, а не
|
||||
* через несколько секунд внутри run_update() (DEBUG_LOG_PHASE3_SD.md).
|
||||
* Игнорируется при recovery_mode == true.
|
||||
* @param recovery_mode Ослабленный version-gate (Фаза 6b) — см.
|
||||
* update_policy_decide(). Решение "входить ли в recovery" принимает
|
||||
* вызывающий код (recovery_decide(), recovery.h), не эта функция.
|
||||
*
|
||||
* @retval true Кандидат успешно установлен и прошёл финальный крипто-гейт
|
||||
* (slot_version_get() на целевом слоте) — в целевом слоте
|
||||
* теперь новый валидный образ.
|
||||
* @retval false Ничего не установлено (SD не вставлена/нет файла/skip) либо
|
||||
* установка не удалась/кандидат отклонён — целевой слот не
|
||||
* изменился относительно состояния до вызова.
|
||||
*
|
||||
* @pre bsp_qspi_init() уже вызван.
|
||||
*/
|
||||
bool sd_update_check(bool downgrade_button_held, bool recovery_mode);
|
||||
|
||||
#endif /* SD_UPDATE_H_ */
|
||||
65
firmware/bootloader/src/slot_version.c
Normal file
65
firmware/bootloader/src/slot_version.c
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* @file slot_version.c
|
||||
* @brief Реализация — см. slot_version.h.
|
||||
*
|
||||
* Вызов bootutil_img_validate() зеркалит loader.c::boot_image_check()
|
||||
* (enc_state=NULL — шифрование образов не используется, image_index=0 —
|
||||
* MCUBOOT_IMAGE_NUMBER=1, seed=NULL/0 — FIH_PROFILE_LOW не использует RNG-
|
||||
* задержку, out_hash=NULL — хэш нам не нужен, только факт валидности+версия).
|
||||
*
|
||||
* FIH_CALL безопасен вне boot_go(): CFI-счётчик (FIH_ENABLE_CFI под LOW
|
||||
* профилем) сохраняется/инкрементируется в FIH_CFI_PRECALL_BLOCK и
|
||||
* проверяется/возвращается к сохранённому значению в FIH_CFI_POSTCALL_BLOCK —
|
||||
* пара самобалансирующаяся на каждый вызов, не накапливающееся состояние
|
||||
* между вызовами (см. fault_injection_hardening.h). Несколько вызовов подряд
|
||||
* (Slot A, Slot Б, кандидат) и последующий отдельный boot_go() не влияют друг
|
||||
* на друга через этот счётчик.
|
||||
*/
|
||||
|
||||
#include "slot_version.h"
|
||||
|
||||
#include "bootutil/fault_injection_hardening.h"
|
||||
#include "flash_map.h"
|
||||
|
||||
#include <stddef.h> /* NULL */
|
||||
|
||||
/*
|
||||
* Совпадает с BOOT_TMPBUF_SZ в sdk/middleware/mcuboot_opensource/boot/bootutil/
|
||||
* src/bootutil_priv.h — приватный заголовок bootutil (в src/, не в include/),
|
||||
* поэтому не включаем его напрямую. Тот же размер, что loader.c использует
|
||||
* для этого же вызова.
|
||||
*/
|
||||
#define SLOT_VERSION_TMPBUF_SIZE 256U
|
||||
|
||||
bool slot_version_get(uint8_t fa_id, struct image_version *p_out_ver)
|
||||
{
|
||||
const struct flash_area *p_fap;
|
||||
if (flash_area_open(fa_id, &p_fap) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
struct image_header hdr;
|
||||
bool read_ok = (flash_area_read(p_fap, 0U, &hdr, sizeof(hdr)) == 0);
|
||||
|
||||
if (!read_ok || (hdr.ih_magic != IMAGE_MAGIC))
|
||||
{
|
||||
flash_area_close(p_fap);
|
||||
return false;
|
||||
}
|
||||
|
||||
static uint8_t s_tmpbuf[SLOT_VERSION_TMPBUF_SIZE];
|
||||
fih_ret fih_rc;
|
||||
FIH_CALL(bootutil_img_validate, fih_rc, NULL, 0, &hdr, p_fap, s_tmpbuf, sizeof(s_tmpbuf), NULL,
|
||||
0, NULL);
|
||||
|
||||
flash_area_close(p_fap);
|
||||
|
||||
if (!FIH_EQ(fih_rc, FIH_SUCCESS))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*p_out_ver = hdr.ih_ver;
|
||||
return true;
|
||||
}
|
||||
37
firmware/bootloader/src/slot_version.h
Normal file
37
firmware/bootloader/src/slot_version.h
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* @file slot_version.h
|
||||
* @brief Read-only "пик" версии образа в слоте bootutil — без побочных
|
||||
* эффектов на flash.
|
||||
*
|
||||
* В отличие от boot_go(): для образа, ни разу не подтверждённого приложением,
|
||||
* bootutil (Direct-XIP-Revert) пишет copy_done в трейлер слота уже на этапе
|
||||
* выбора — вызов boot_go() второй раз за одну сессию питания принял бы этот
|
||||
* флаг за "образ уже грузился и не подтвердился" и стёр бы его (см.
|
||||
* firmware/bootloader/PLAN.md, Фаза 3). slot_version_get() читает и валидирует
|
||||
* слот через flash_area_read()/bootutil_img_validate() напрямую — оба
|
||||
* read-only (image_validate.c не пишет в flash), в обход boot_go().
|
||||
*
|
||||
* Валидация — полная (hash + ECDSA-подпись через bootutil_img_validate()),
|
||||
* тот же путь, что loader.c::boot_image_check() использует для каждого слота
|
||||
* при штатной загрузке, а не самодельная проверка одного заголовка.
|
||||
*/
|
||||
|
||||
#ifndef SLOT_VERSION_H_
|
||||
#define SLOT_VERSION_H_
|
||||
|
||||
#include "bootutil/image.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @brief Прочитать и провалидировать образ в слоте fa_id.
|
||||
*
|
||||
* @param[in] fa_id ID области (см. sysflash.h) — 0 = Slot A, 1 = Slot Б.
|
||||
* @param[out] p_out_ver Версия образа при успехе. Не тронут при false.
|
||||
* @retval true Валидный образ (magic, hash и подпись прошли) — версия в *p_out_ver.
|
||||
* @retval false Слот пуст/повреждён/подпись не прошла, либо ошибка чтения/открытия.
|
||||
*/
|
||||
bool slot_version_get(uint8_t fa_id, struct image_version *p_out_ver);
|
||||
|
||||
#endif /* SLOT_VERSION_H_ */
|
||||
110
firmware/bootloader/src/update_policy.c
Normal file
110
firmware/bootloader/src/update_policy.c
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* @file update_policy.c
|
||||
* @brief Реализация — см. update_policy.h.
|
||||
*/
|
||||
|
||||
#include "update_policy.h"
|
||||
|
||||
/* ── Сравнение версий ──────────────────────────────────────────────────── */
|
||||
|
||||
int image_version_compare(const struct image_version *p_ver1, const struct image_version *p_ver2)
|
||||
{
|
||||
if (p_ver1->iv_major != p_ver2->iv_major)
|
||||
{
|
||||
return (p_ver1->iv_major > p_ver2->iv_major) ? 1 : -1;
|
||||
}
|
||||
|
||||
if (p_ver1->iv_minor != p_ver2->iv_minor)
|
||||
{
|
||||
return (p_ver1->iv_minor > p_ver2->iv_minor) ? 1 : -1;
|
||||
}
|
||||
|
||||
if (p_ver1->iv_revision != p_ver2->iv_revision)
|
||||
{
|
||||
return (p_ver1->iv_revision > p_ver2->iv_revision) ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── Определение активного слота ──────────────────────────────────────── */
|
||||
|
||||
typedef struct
|
||||
{
|
||||
bool have_active;
|
||||
update_policy_slot_t active_slot;
|
||||
struct image_version active_ver;
|
||||
} active_slot_info_t;
|
||||
|
||||
static active_slot_info_t find_active_slot(const update_policy_slot_state_t *p_slot_a,
|
||||
const update_policy_slot_state_t *p_slot_b)
|
||||
{
|
||||
active_slot_info_t info = { .have_active = false };
|
||||
|
||||
if (p_slot_a->valid)
|
||||
{
|
||||
info.have_active = true;
|
||||
info.active_slot = UPDATE_POLICY_SLOT_A;
|
||||
info.active_ver = p_slot_a->version;
|
||||
}
|
||||
|
||||
if (p_slot_b->valid &&
|
||||
(!info.have_active || image_version_compare(&p_slot_b->version, &info.active_ver) > 0))
|
||||
{
|
||||
info.have_active = true;
|
||||
info.active_slot = UPDATE_POLICY_SLOT_B;
|
||||
info.active_ver = p_slot_b->version;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
static update_policy_slot_t other_slot(update_policy_slot_t slot)
|
||||
{
|
||||
return (slot == UPDATE_POLICY_SLOT_A) ? UPDATE_POLICY_SLOT_B : UPDATE_POLICY_SLOT_A;
|
||||
}
|
||||
|
||||
/* ── Публичный API ─────────────────────────────────────────────────────── */
|
||||
|
||||
update_policy_result_t update_policy_decide(const update_policy_slot_state_t *p_slot_a,
|
||||
const update_policy_slot_state_t *p_slot_b,
|
||||
const struct image_version *p_candidate_ver,
|
||||
bool button_held, bool recovery_mode)
|
||||
{
|
||||
if (recovery_mode)
|
||||
{
|
||||
/* Ослабленный гейт: версия/кнопка не участвуют, целевой слот всегда
|
||||
* A, Slot Б обязан быть стёрт (см. recovery.h — этот флаг не связан
|
||||
* с recovery_decide() там). */
|
||||
return (update_policy_result_t) { .action = UPDATE_POLICY_INSTALL,
|
||||
.target_slot = UPDATE_POLICY_SLOT_A,
|
||||
.erase_previous_active = true };
|
||||
}
|
||||
|
||||
active_slot_info_t active = find_active_slot(p_slot_a, p_slot_b);
|
||||
|
||||
if (!active.have_active)
|
||||
{
|
||||
return (update_policy_result_t) { .action = UPDATE_POLICY_INSTALL,
|
||||
.target_slot = UPDATE_POLICY_SLOT_A,
|
||||
.erase_previous_active = false };
|
||||
}
|
||||
|
||||
int cmp = image_version_compare(p_candidate_ver, &active.active_ver);
|
||||
|
||||
bool candidate_newer = (cmp > 0);
|
||||
bool forced_downgrade = (cmp < 0) && button_held;
|
||||
|
||||
if (!candidate_newer && !forced_downgrade)
|
||||
{
|
||||
return (update_policy_result_t) { .action = UPDATE_POLICY_SKIP };
|
||||
}
|
||||
|
||||
/* Форс. даунгрейд без стирания прежнего активного слота не имел бы
|
||||
* эффекта — он остаётся валиден и новее, и снова выиграет в boot_go().
|
||||
* "Новее" не требует стирания — прежний активный сам проиграет
|
||||
* сравнение версий естественным путём. */
|
||||
return (update_policy_result_t) { .action = UPDATE_POLICY_INSTALL,
|
||||
.target_slot = other_slot(active.active_slot),
|
||||
.erase_previous_active = forced_downgrade };
|
||||
}
|
||||
116
firmware/bootloader/src/update_policy.h
Normal file
116
firmware/bootloader/src/update_policy.h
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* @file update_policy.h
|
||||
* @brief Чистая логика решения "устанавливать ли SD-кандидат" — без
|
||||
* аппаратных зависимостей (flash/FatFS), полностью host-тестируема.
|
||||
*
|
||||
* См. firmware/bootloader/PLAN.md, Фаза 3.
|
||||
*/
|
||||
|
||||
#ifndef UPDATE_POLICY_H_
|
||||
#define UPDATE_POLICY_H_
|
||||
|
||||
#include "bootutil/image.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/** @brief Логический слот (индекс сисфлеша, не физический адрес). */
|
||||
typedef enum
|
||||
{
|
||||
UPDATE_POLICY_SLOT_A = 0,
|
||||
UPDATE_POLICY_SLOT_B = 1,
|
||||
} update_policy_slot_t;
|
||||
|
||||
/** @brief Состояние одного слота глазами вызывающего (см. slot_version.h). */
|
||||
typedef struct
|
||||
{
|
||||
bool valid; /**< Есть валидный (прошедший bootutil_img_validate) образ. */
|
||||
struct image_version version; /**< Значимо только если valid == true. */
|
||||
} update_policy_slot_state_t;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
UPDATE_POLICY_SKIP = 0,
|
||||
UPDATE_POLICY_INSTALL,
|
||||
} update_policy_action_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
update_policy_action_t action;
|
||||
update_policy_slot_t target_slot; /**< Значим только если action == UPDATE_POLICY_INSTALL. */
|
||||
|
||||
/**
|
||||
* Значим только при action == UPDATE_POLICY_INSTALL. Если true —
|
||||
* вызывающий код обязан, ПОСЛЕ успешной установки и пост-записи
|
||||
* валидации target_slot, стереть слот, который был активным ДО
|
||||
* установки (см. rationale ниже про форс. даунгрейд).
|
||||
*/
|
||||
bool erase_previous_active;
|
||||
} update_policy_result_t;
|
||||
|
||||
/**
|
||||
* @brief Решить, устанавливать ли SD-кандидат, и в какой слот.
|
||||
*
|
||||
* Правила (обычный режим, recovery_mode == false):
|
||||
* - "Активный" слот — валидный слот с более высокой версией; если валиден
|
||||
* только один — он активный; если ни одного — активного слота нет.
|
||||
* - Целевой слот установки — всегда НЕ активный (активный не перезаписываем
|
||||
* никогда, независимо от исхода сравнения версий). Если активного слота
|
||||
* нет — по умолчанию Slot A.
|
||||
* - Кандидат новее активного (или активного слота нет вообще) → INSTALL,
|
||||
* erase_previous_active = false. Прежний активный слот сам проиграет
|
||||
* сравнение версий в boot_go() — стирать его не нужно.
|
||||
* - Кандидат старше или равен активному, кнопка не удержана → SKIP.
|
||||
* - Кандидат старше активного, кнопка удержана → INSTALL,
|
||||
* erase_previous_active = true. Без этого форс. даунгрейд не имел бы
|
||||
* эффекта: прежний (более новый) активный слот остался бы валиден и
|
||||
* снова выиграл бы сравнение версий в boot_go(), несмотря на успешную
|
||||
* запись более старого образа в другой слот. Стирание — обязанность
|
||||
* вызывающего кода и только ПОСЛЕ подтверждения, что только что
|
||||
* установленный образ валиден (иначе на короткое время не осталось бы
|
||||
* ни одного рабочего слота).
|
||||
* - Кандидат равен активному, кнопка удержана → SKIP (не форсируем
|
||||
* переустановку той же версии).
|
||||
*
|
||||
* Recovery-режим (recovery_mode == true, Фаза 6b) — ослабленный version-gate:
|
||||
* - Версия кандидата и button_held игнорируются целиком — ЛЮБОЙ кандидат,
|
||||
* прошедший последующие (внешние по отношению к этой функции) проверки
|
||||
* заголовка и крипто-гейта, принимается безусловно.
|
||||
* - target_slot — всегда Slot A, независимо от того, какой слот был активен
|
||||
* до входа в recovery (в отличие от обычного режима, где целевой слот
|
||||
* вычисляется как "не активный").
|
||||
* - erase_previous_active — всегда true: вызывающий код обязан стереть
|
||||
* Slot Б перед/после установки в Slot A (см. rationale выше про порядок
|
||||
* "стереть только после подтверждения валидности нового образа") — вместе
|
||||
* с тем, что erase_and_copy_candidate() и так стирает сам target_slot
|
||||
* перед записью, это и даёт "чистый борт": оба слота гарантированно
|
||||
* стёрты, в Slot A — только что установленный и провалидированный образ.
|
||||
*
|
||||
* @param[in] p_slot_a Состояние Slot A.
|
||||
* @param[in] p_slot_b Состояние Slot Б.
|
||||
* @param[in] p_candidate_ver Версия образа-кандидата на SD. Не читается при
|
||||
* recovery_mode == true.
|
||||
* @param[in] button_held Кнопка даунгрейда (BSP_BUTTON_1) удержана на старте.
|
||||
* Не читается при recovery_mode == true.
|
||||
* @param[in] recovery_mode Ослабленный version-gate (см. выше). Не путать с
|
||||
* recovery_decide() (recovery.h) — та решает,
|
||||
* входить ли в recovery-режим вообще (счётчик
|
||||
* watchdog-сбросов), эта функция — что делать с
|
||||
* SD-кандидатом, уже находясь в нём.
|
||||
*/
|
||||
update_policy_result_t update_policy_decide(const update_policy_slot_state_t *p_slot_a,
|
||||
const update_policy_slot_state_t *p_slot_b,
|
||||
const struct image_version *p_candidate_ver,
|
||||
bool button_held, bool recovery_mode);
|
||||
|
||||
/**
|
||||
* @brief Сравнить версии образов: major.minor.revision, без build_num — то
|
||||
* же соглашение, что boot_version_cmp() в sdk/.../bootutil/loader.c
|
||||
* (static там, не экспортируется — здесь свой аналог).
|
||||
*
|
||||
* @retval <0 p_ver1 < p_ver2
|
||||
* @retval 0 p_ver1 == p_ver2
|
||||
* @retval >0 p_ver1 > p_ver2
|
||||
*/
|
||||
int image_version_compare(const struct image_version *p_ver1, const struct image_version *p_ver2);
|
||||
|
||||
#endif /* UPDATE_POLICY_H_ */
|
||||
25
firmware/bootloader/src/version.h.in
Normal file
25
firmware/bootloader/src/version.h.in
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* @file version.h
|
||||
* @brief Версия bootloader — генерируется CMake из CMakeLists.txt.
|
||||
*
|
||||
* НЕ редактировать вручную. Версию менять в firmware/bootloader/CMakeLists.txt:
|
||||
* project(bootloader VERSION X.Y.Z)
|
||||
*/
|
||||
|
||||
#ifndef VERSION_H_
|
||||
#define VERSION_H_
|
||||
|
||||
/** @brief Мажорная версия. */
|
||||
#define BOOTLOADER_VERSION_MAJOR @bootloader_VERSION_MAJOR@
|
||||
|
||||
/** @brief Минорная версия. */
|
||||
#define BOOTLOADER_VERSION_MINOR @bootloader_VERSION_MINOR@
|
||||
|
||||
/** @brief Патч-версия. */
|
||||
#define BOOTLOADER_VERSION_PATCH @bootloader_VERSION_PATCH@
|
||||
|
||||
/** @brief Версия строкой для протокола: "X.Y.Z". */
|
||||
#define BOOTLOADER_VERSION_STR \
|
||||
"@bootloader_VERSION_MAJOR@.@bootloader_VERSION_MINOR@.@bootloader_VERSION_PATCH@"
|
||||
|
||||
#endif /* VERSION_H_ */
|
||||
186
firmware/bootloader/test_stub/CMakeLists.txt
Normal file
186
firmware/bootloader/test_stub/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
# firmware/bootloader/test_stub/CMakeLists.txt
|
||||
#
|
||||
# Заглушка tft_app для аппаратной верификации корректной работы загрузчика
|
||||
#
|
||||
# Один main.c, много таргетов: адрес слота (--defsym __slot_base__) + частота
|
||||
# мигания (STUB_BLINK_MS) — чтобы на глаз отличить, какой слот выбрал bootloader
|
||||
# плюс три независимые оси confirm/hang/watchdog — чтобы прогнать классы
|
||||
# отказов A/B на реальном железе. Подписывается вручную imgtool.
|
||||
#
|
||||
include(
|
||||
${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/bootutil_sources.cmake)
|
||||
|
||||
function(add_mcuboot_stub)
|
||||
cmake_parse_arguments(
|
||||
ARG ""
|
||||
"NAME;SLOT_BASE;BLINK_MS;OWN_SLOT_ID;CONFIRM_MODE;HANG_MODE;FEED_WDOG" ""
|
||||
${ARGN})
|
||||
|
||||
if(NOT DEFINED ARG_CONFIRM_MODE)
|
||||
set(ARG_CONFIRM_MODE 0) # 0 сразу / 1 отложенно / 2 никогда — см. main.c
|
||||
endif()
|
||||
if(NOT DEFINED ARG_HANG_MODE)
|
||||
set(ARG_HANG_MODE 0) # 0 никогда / 1 до health-mark / 2 после health-mark /
|
||||
# 3 после confirm
|
||||
endif()
|
||||
if(NOT DEFINED ARG_FEED_WDOG)
|
||||
set(ARG_FEED_WDOG 1) # 1 кормить (дефолт, как в проде) / 0 не кормить
|
||||
endif()
|
||||
|
||||
add_executable(
|
||||
${ARG_NAME}
|
||||
main.c
|
||||
${BSP_GENERATED}/clock_config.c
|
||||
${BSP_STARTUP_FILE}
|
||||
${BSP_SYSCALLS_FILE}
|
||||
${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/flash_map_backend.c
|
||||
${CMAKE_SOURCE_DIR}/firmware/bootloader/src/led_status.c
|
||||
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c)
|
||||
|
||||
# led_status.h — flash_map_backend.c (общий с bootloader) зовёт
|
||||
# led_status_tick_install() из поблочного erase (см. led_status.h) — не
|
||||
# добавляет стабу нового поведения (окно установки здесь никогда не
|
||||
# открыто, tick — no-op), просто нужен для компиляции общего файла.
|
||||
target_include_directories(
|
||||
${ARG_NAME} PRIVATE ${MCUBOOT_BOOTUTIL_INCLUDES}
|
||||
${CMAKE_SOURCE_DIR}/firmware/bootloader/src)
|
||||
|
||||
target_compile_definitions(
|
||||
${ARG_NAME}
|
||||
PRIVATE STUB_BLINK_MS=${ARG_BLINK_MS}
|
||||
STUB_OWN_SLOT_ID=${ARG_OWN_SLOT_ID}
|
||||
STUB_CONFIRM_MODE=${ARG_CONFIRM_MODE}
|
||||
STUB_HANG_MODE=${ARG_HANG_MODE}
|
||||
STUB_FEED_WDOG=${ARG_FEED_WDOG}
|
||||
__STARTUP_CLEAR_BSS)
|
||||
|
||||
# Вендоренный bootutil_public.c — не наш стиль/warnings, тот же обход, что и
|
||||
# для остального bootutil (см. bootutil_sources.cmake). Наш код (main.c,
|
||||
# flash_map_backend.c) остаётся под обычными warnings.
|
||||
set_source_files_properties(
|
||||
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c
|
||||
PROPERTIES COMPILE_OPTIONS "${MCUBOOT_VENDORED_COMPILE_OPTIONS}")
|
||||
|
||||
# bsp_wdog: загрузчик взводит WDOG перед прыжком, WDE — write-once, поэтому
|
||||
# образ ОБЯЗАН его кормить, если STUB_FEED_WDOG=1 (иначе reset-loop — либо
|
||||
# намеренно, для теста самого механизма). bsp_boot_state —
|
||||
# bsp_boot_health_mark(). bsp_qspi_flash — нужен flash_map_backend.c для
|
||||
# confirm_self().
|
||||
target_link_libraries(
|
||||
${ARG_NAME}
|
||||
PRIVATE bsp_board
|
||||
bsp_led
|
||||
bsp_tick
|
||||
bsp_boot_xip_no_dcd
|
||||
bsp_wdog
|
||||
bsp_boot_state
|
||||
bsp_qspi_flash)
|
||||
|
||||
target_link_options(
|
||||
${ARG_NAME}
|
||||
PRIVATE
|
||||
-Wl,--gc-sections
|
||||
-Wl,--print-memory-usage
|
||||
-Wl,-Map=${CMAKE_BINARY_DIR}/${ARG_NAME}.map
|
||||
-Wl,--defsym=__slot_base__=${ARG_SLOT_BASE}
|
||||
-Wl,--defsym=__stack_size__=0x400
|
||||
-Wl,--defsym=__heap_size__=0x400
|
||||
-T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld)
|
||||
|
||||
set_target_properties(${ARG_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY
|
||||
${CMAKE_BINARY_DIR})
|
||||
|
||||
add_custom_command(
|
||||
TARGET ${ARG_NAME}
|
||||
POST_BUILD
|
||||
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${ARG_NAME}>
|
||||
${CMAKE_BINARY_DIR}/${ARG_NAME}.bin
|
||||
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${ARG_NAME}>
|
||||
COMMENT "Generating ${ARG_NAME}.bin")
|
||||
endfunction()
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Базовые "здоровые" заглушки: подтверждаются сразу, никогда не виснут. Slot A
|
||||
# мигает раз в 500 мс ("версия 1"), Slot Б — раз в 250 мс ("версия 2").
|
||||
# CONFIRM_MODE/HANG_MODE/FEED_WDOG — дефолты (0/0/1).
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
add_mcuboot_stub(
|
||||
NAME
|
||||
test_slot_stub_a
|
||||
SLOT_BASE
|
||||
0x60040000
|
||||
BLINK_MS
|
||||
500
|
||||
OWN_SLOT_ID
|
||||
0)
|
||||
add_mcuboot_stub(
|
||||
NAME
|
||||
test_slot_stub_b
|
||||
SLOT_BASE
|
||||
0x60240000
|
||||
BLINK_MS
|
||||
250
|
||||
OWN_SLOT_ID
|
||||
1)
|
||||
|
||||
# Класс A: никогда не подтверждается, виснет СРАЗУ (до health-mark) — свежий
|
||||
# неподтверждённый образ виснет до всякого прогресса. WDOG-сброс → штатный
|
||||
# MCUboot revert (copy_done без image_ok) → откат, без участия recovery.c.
|
||||
add_mcuboot_stub(
|
||||
NAME
|
||||
test_slot_stub_a_hang
|
||||
SLOT_BASE
|
||||
0x60040000
|
||||
BLINK_MS
|
||||
500
|
||||
OWN_SLOT_ID
|
||||
0
|
||||
CONFIRM_MODE
|
||||
2
|
||||
HANG_MODE
|
||||
1)
|
||||
add_mcuboot_stub(
|
||||
NAME
|
||||
test_slot_stub_b_hang
|
||||
SLOT_BASE
|
||||
0x60240000
|
||||
BLINK_MS
|
||||
250
|
||||
OWN_SLOT_ID
|
||||
1
|
||||
CONFIRM_MODE
|
||||
2
|
||||
HANG_MODE
|
||||
1)
|
||||
|
||||
# Класс B: подтверждается СРАЗУ (успевает помигать наблюдаемо — видно, что образ
|
||||
# живой и подтверждён), виснет через STUB_CONFIRM_DELAY_MS + STUB_HANG_DELAY_MS
|
||||
# (~6 c) после старта — уже ПОСЛЕ confirm. Штатный revert MCUboot тут не
|
||||
# сработает (image_ok уже SET) — ловит именно recovery.c (счётчик
|
||||
# watchdog-сбросов + фолбэк/recovery).
|
||||
add_mcuboot_stub(
|
||||
NAME
|
||||
test_slot_stub_a_confirm_hang
|
||||
SLOT_BASE
|
||||
0x60040000
|
||||
BLINK_MS
|
||||
500
|
||||
OWN_SLOT_ID
|
||||
0
|
||||
CONFIRM_MODE
|
||||
0
|
||||
HANG_MODE
|
||||
3)
|
||||
add_mcuboot_stub(
|
||||
NAME
|
||||
test_slot_stub_b_confirm_hang
|
||||
SLOT_BASE
|
||||
0x60240000
|
||||
BLINK_MS
|
||||
250
|
||||
OWN_SLOT_ID
|
||||
1
|
||||
CONFIRM_MODE
|
||||
0
|
||||
HANG_MODE
|
||||
3)
|
||||
161
firmware/bootloader/test_stub/main.c
Normal file
161
firmware/bootloader/test_stub/main.c
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/**
|
||||
* @file main.c
|
||||
* @brief Заглушка tft_app для аппаратной верификации bootutil (Фаза 2) и
|
||||
* recovery-логики (Фаза 6).
|
||||
*
|
||||
* tft_app ещё не реализована — bootloader'у некуда прыгать. Этот образ —
|
||||
* минимальный, но настоящий imgtool-подписанный XIP-образ с корректным
|
||||
* vector table по адресу слота: базово мигает LED_APP с периодом,
|
||||
* зависящим от STUB_BLINK_MS (задаётся компилятору), чтобы по частоте
|
||||
* мигания визуально отличить, какой слот реально выбрал bootloader. См.
|
||||
* firmware/bootloader/PLAN.md, "Аппаратная верификация Фазы 2".
|
||||
*
|
||||
* Фаза 6 добавила три независимые, настраиваемые компилятором оси —
|
||||
* стенд-контракт tft_app (см. PLAN.md, 6c) минимально, но по-настоящему:
|
||||
* - STUB_CONFIRM_MODE — 0 сразу / 1 отложенно (STUB_CONFIRM_DELAY_MS) /
|
||||
* 2 никогда. Подтверждение — boot_set_next(fap, true, true) на
|
||||
* СОБСТВЕННОМ слоте (STUB_OWN_SLOT_ID), НЕ boot_set_confirmed() — та
|
||||
* жёстко пишет в FLASH_AREA_IMAGE_PRIMARY (Slot A) независимо от того,
|
||||
* откуда реально исполняется код: для стаба в Slot Б это подтвердило бы
|
||||
* чужой слот, а не себя.
|
||||
* - STUB_HANG_MODE — 0 никогда / 1 до health-mark (сразу на входе) /
|
||||
* 2 после health-mark, до confirm / 3 после confirm (через
|
||||
* STUB_HANG_DELAY_MS после факта подтверждения — чтобы успеть увидеть
|
||||
* мигание глазами перед тем как оно застынет).
|
||||
* - STUB_FEED_WDOG — 1 (дефолт, как в проде) / 0 — не кормить watchdog,
|
||||
* детерминированно проверить сам механизм сброса.
|
||||
*
|
||||
* Без USB/CDC — визуальной индикации (частота/застывание LED_APP) достаточно
|
||||
* для чек-листа Фазы 6, минимальный код.
|
||||
*
|
||||
* Watchdog: загрузчик взводит аппаратный WDOG перед прыжком сюда, а WDE —
|
||||
* write-once (выключить нельзя). Поэтому заглушка, если сконфигурирована
|
||||
* его кормить (STUB_FEED_WDOG=1, дефолт), обязана делать это в главном цикле
|
||||
* — иначе WDOG сбросит плату через таймаут (см. bsp/wdog/README.md).
|
||||
*/
|
||||
#include "board.h"
|
||||
#include "bootutil/bootutil_public.h"
|
||||
#include "bsp/boot_state.h"
|
||||
#include "bsp/led.h"
|
||||
#include "bsp/tick.h"
|
||||
#include "bsp/wdog.h"
|
||||
#include "flash_map.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifndef STUB_BLINK_MS
|
||||
#error "STUB_BLINK_MS must be defined (see firmware/bootloader/test_stub/CMakeLists.txt)"
|
||||
#endif
|
||||
|
||||
#ifndef STUB_OWN_SLOT_ID
|
||||
#error "STUB_OWN_SLOT_ID must be defined (0 = Slot A, 1 = Slot Б)"
|
||||
#endif
|
||||
|
||||
/* Confirm mode: 0 = сразу, 1 = отложенно, 2 = никогда. */
|
||||
#ifndef STUB_CONFIRM_MODE
|
||||
#define STUB_CONFIRM_MODE 0
|
||||
#endif
|
||||
|
||||
#ifndef STUB_CONFIRM_DELAY_MS
|
||||
#define STUB_CONFIRM_DELAY_MS 3000U
|
||||
#endif
|
||||
|
||||
/* Hang mode: 0 = никогда, 1 = до health-mark, 2 = после health-mark (до
|
||||
* confirm), 3 = после confirm. */
|
||||
#ifndef STUB_HANG_MODE
|
||||
#define STUB_HANG_MODE 0
|
||||
#endif
|
||||
|
||||
#ifndef STUB_HANG_DELAY_MS
|
||||
#define STUB_HANG_DELAY_MS 3000U
|
||||
#endif
|
||||
|
||||
#ifndef STUB_FEED_WDOG
|
||||
#define STUB_FEED_WDOG 1
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Подтвердить СОБСТВЕННЫЙ слот (STUB_OWN_SLOT_ID).
|
||||
*
|
||||
* boot_set_next(fap, active=true, confirm=true) — не boot_set_confirmed():
|
||||
* та жёстко работает с FLASH_AREA_IMAGE_PRIMARY (Slot A) вне зависимости от
|
||||
* того, какой слот реально исполняется; для Direct-XIP с двумя равноправными
|
||||
* слотами это подтвердило бы не тот слот при исполнении из Slot Б.
|
||||
*/
|
||||
static void confirm_self(void)
|
||||
{
|
||||
const struct flash_area *p_fap;
|
||||
if (flash_area_open((uint8_t) STUB_OWN_SLOT_ID, &p_fap) == 0)
|
||||
{
|
||||
(void) boot_set_next(p_fap, true, true);
|
||||
flash_area_close(p_fap);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
board_hw_init();
|
||||
bsp_led_init();
|
||||
bsp_tick_init();
|
||||
|
||||
#if STUB_HANG_MODE == 1
|
||||
for (;;) { } /* до health-mark — Класс A: свежий образ виснет сразу */
|
||||
#endif
|
||||
|
||||
/* health-mark == bsp_boot_attempt_reset() (см. bsp/boot_state.h) — вызывать
|
||||
* его безусловно перед потенциальным зависанием НЕЛЬЗЯ: он обнулял бы
|
||||
* счётчик попыток на КАЖДОМ цикле ДО того, как зависание успевает
|
||||
* засчитаться, и recovery-фолбэк/recovery-режим не сработали бы никогда
|
||||
* (найдено на железе — HANG_MODE=3 резетился бесконечно вместо остановки
|
||||
* на пороге). Это тот же класс "честной границы", что уже описан в
|
||||
* PLAN.md (образ обнуляет счётчик, ПОТОМ виснет — таймером не отличить
|
||||
* "здоров" от "здоров, но детерминированно виснет"): здесь HANG_MODE
|
||||
* снимает эту неоднозначность на этапе компиляции — если конфигурация
|
||||
* гарантированно виснет (2 — после health-mark, до confirm; 3 — после
|
||||
* confirm), health-mark не зовём вообще, счётчик копится корректно.
|
||||
* Здоровый стаб (HANG_MODE 0) и "виснет до health-mark" (1, сюда и не
|
||||
* доходит) — не затронуты. */
|
||||
#if (STUB_HANG_MODE != 2) && (STUB_HANG_MODE != 3)
|
||||
bsp_boot_health_mark(); /* "дошёл до устойчивого состояния" (Фаза 6, 6c) */
|
||||
#endif
|
||||
|
||||
#if STUB_HANG_MODE == 2
|
||||
for (;;) { } /* после (несостоявшегося) health-mark, до confirm */
|
||||
#endif
|
||||
|
||||
bool confirmed = false;
|
||||
|
||||
#if STUB_CONFIRM_MODE == 0
|
||||
confirm_self();
|
||||
confirmed = true;
|
||||
#endif
|
||||
|
||||
uint32_t start_ms = bsp_tick_get_ms();
|
||||
(void) start_ms; /* не используется, если ни один из режимов ниже её не читает */
|
||||
|
||||
while (1)
|
||||
{
|
||||
#if STUB_FEED_WDOG
|
||||
bsp_wdog_refresh(); /* обслуживаем унаследованный от загрузчика WDOG */
|
||||
#endif
|
||||
|
||||
#if STUB_CONFIRM_MODE == 1
|
||||
if (!confirmed && ((bsp_tick_get_ms() - start_ms) >= STUB_CONFIRM_DELAY_MS))
|
||||
{
|
||||
confirm_self();
|
||||
confirmed = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if STUB_HANG_MODE == 3
|
||||
if (confirmed &&
|
||||
((bsp_tick_get_ms() - start_ms) >= (STUB_CONFIRM_DELAY_MS + STUB_HANG_DELAY_MS)))
|
||||
{
|
||||
for (;;) { } /* после confirm — Класс B: подтверждённый образ виснет в рантайме */
|
||||
}
|
||||
#endif
|
||||
|
||||
bsp_led_toggle(LED_APP);
|
||||
bsp_delay(STUB_BLINK_MS);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
cmake_minimum_required(VERSION 3.20)
|
||||
project(
|
||||
firmware_test
|
||||
VERSION 0.1.1
|
||||
VERSION 0.1.2
|
||||
LANGUAGES C ASM)
|
||||
|
||||
set(TARGET_NAME firmware_test)
|
||||
|
|
|
|||
|
|
@ -1,488 +0,0 @@
|
|||
# firmware_test — План разработки
|
||||
|
||||
> Версия: 0.8 | Обновлён после завершения Этапа 6г (bsp_mqs + test_mqs, hardware-verified).
|
||||
|
||||
---
|
||||
|
||||
## Контекст проекта
|
||||
|
||||
**Цель прошивки:** диагностика платы MIMXRT1052CVJ5B на сервисе (возврат по рекламации).
|
||||
Запускается через BootROM (USB SDP), без предварительной прошивки загрузчика.
|
||||
|
||||
**Стенд:**
|
||||
|
||||
- Хост подключается через USB CDC ACM — единственный канал firmware_test
|
||||
- HIL-тесты управляются через M5StampPLC (опционально)
|
||||
- TUI-приложение оркестрирует и firmware_test (CDC), и M5 (Serial) одновременно
|
||||
|
||||
---
|
||||
|
||||
## Текущий статус
|
||||
|
||||
| Компонент | Статус | Примечание |
|
||||
| ------------------------------ | ------ | ------------------------------------------------ |
|
||||
| `bsp_usb_cdc` | ✅ | HIL тест пройден |
|
||||
| firmware_test скелет | ✅ | `main.c` + `cli.c` |
|
||||
| Протокол v2 + test_runner | ✅ | JSON-lines event-driven |
|
||||
| `bsp_sdram` + `test_sdram` | ✅ | 4 фазы: addr/data/seq/retention |
|
||||
| `bsp_qspi_flash` + `test_qspi` | ✅ | JEDEC + erase + rw + addr range |
|
||||
| `bsp_sd` + `test_usd` | ✅ | bsp_sd + FatFS, pre_confirm, 4 шага |
|
||||
| `bsp_display` + `test_display` | ✅ | 4 цвета + ротация, hardware-verified |
|
||||
| `bsp_button` + `test_buttons` | ✅ | 2 кнопки, physical detect, hardware-verified |
|
||||
| Протокол: `list_tests` | ✅ | Этап 6а, hardware-verified |
|
||||
| Протокол: `run_selected` | ✅ | Этап 6а, hardware-verified |
|
||||
| `test_opto` | ✅ | Этап 6б, hardware-verified |
|
||||
| `test_can` | ✅ | Этап 6в, hardware-verified |
|
||||
| HIL pytest firmware_cdc | ✅ | Этап 6д, `FirmwareCdc` + `firmware_cdc` фикстура |
|
||||
| HIL pytest firmware_opto | ✅ | Этап 6е, `06_test_firmware_opto.py` |
|
||||
| HIL pytest firmware_can | ✅ | Этап 6ж, `06_test_firmware_can.py` |
|
||||
| `bsp_mqs` + `test_mqs` | ✅ | Этап 6г, hardware-verified |
|
||||
| Provisioning | ⬜ | Этап 7 |
|
||||
| TUI сервисного инженера | ⬜ | Этап 8 |
|
||||
|
||||
---
|
||||
|
||||
## Матрица тестов — итоговая
|
||||
|
||||
| ID | Название | Critical | HIL | Тип | BSP | Статус |
|
||||
| --------- | ------------------ | -------- | --- | ----------- | ------------------ | ------ |
|
||||
| `sdram` | SDRAM 32 MB | ✅ | ❌ | self | `bsp_sdram` ✅ | ✅ |
|
||||
| `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | self | `bsp_qspi_flash` ✅ | ✅ |
|
||||
| `usd` | microSD (SDIO) | ❌ | ❌ | interactive | `bsp_sd` ✅ | ✅ |
|
||||
| `display` | TFT Display RGB888 | ❌ | ❌ | interactive | `bsp_display` ✅ | ✅ |
|
||||
| `buttons` | Test Buttons 1/2 | ❌ | ❌ | interactive | `bsp_button` ✅ | ✅ |
|
||||
| `mqs` | MQS Audio Out | ❌ | ❌ | interactive | `bsp_mqs` ✅ | ✅ |
|
||||
| `can` | CAN loopback | ❌ | ✅ | HIL | `bsp_can` ✅ | ✅ |
|
||||
| `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | HIL | `bsp_opto` ✅ | ✅ |
|
||||
|
||||
**Убранные тесты (закрытые решения):**
|
||||
|
||||
- `uart_ttl` — LPUART1 dev-инструмент (MCU-Link VCOM), в сервисе не используется
|
||||
- `uart_iso` — RS_RX физически тот же пин что IN в `test_opto`, избыточно
|
||||
|
||||
---
|
||||
|
||||
## Закрытые архитектурные решения
|
||||
|
||||
> Не пересматривать без явного запроса.
|
||||
|
||||
### Этапы 1–5 (ранее зафиксированные)
|
||||
|
||||
- **Транспорт:** USB CDC ACM — единственный канал. UART не используется в firmware_test.
|
||||
- **Парсинг JSON:** без cJSON, строковый `strstr`. Входящее поле всегда `"type"` / `"cmd"`.
|
||||
- **SDRAM и DCD:** SEMC инициализируется DCD до `main()`. `bsp_sdram_init()` только верифицирует.
|
||||
- **QSPI-функции в ITCM:** `AT_QUICKACCESS_SECTION_CODE` + `__STARTUP_INITIALIZE_RAMFUNCTION`.
|
||||
- **W25Q256/512:** dedicated 4-byte opcodes, без Enter 4-Byte Mode (0xB7).
|
||||
- **bsp_button_init():** вызывается в `init()` тест-модуля, не в `main.c`.
|
||||
- **Тест дисплея:** 4 цвета + 2 ротации. Таймаут confirm 15 с → FAIL.
|
||||
- **Тест кнопок:** физическая детекция через `bsp_button`. Хост не отправляет JSON confirm. Таймаут 10 с → SKIP.
|
||||
|
||||
### Этап 6 (новые решения)
|
||||
|
||||
- **Разделение тестов:** `requires_hil=false` (standalone) vs `requires_hil=true` (HIL).
|
||||
TUI фильтрует HIL-тесты если M5StampPLC не подключён.
|
||||
- **`list_tests`:** таргет отдаёт реестр тестов с метаданными по запросу хоста.
|
||||
TUI строит UI динамически, не хардкодит список тестов.
|
||||
- **`run_selected`:** запуск произвольного подмножества тестов по списку ID.
|
||||
Порядок выполнения — как в реестре таргета, не как в запросе.
|
||||
Таргет принимает любой список без проверки `requires_hil` — ответственность на TUI.
|
||||
- **TUI оркестрирует M5:** firmware_test не знает про M5. При `confirm_request`
|
||||
от HIL-теста TUI командует M5, получает результат, отправляет confirm.
|
||||
- **M5 опционален:** TUI при старте пробует найти M5. Не нашёл — HIL-тесты
|
||||
недоступны (серые в UI, не входят в `run_selected`).
|
||||
- **Фильтрация HIL на стороне TUI:** таргет не фильтрует по `requires_hil`.
|
||||
- **MQS стерео:** MQS MIMXRT1052 требует стерео PCM-буфер. На плате выведен
|
||||
один канал. Буфер всегда стерео (L+R идентичны).
|
||||
- **MQS тест:** захардкоженная мелодия ~4 с (A4 + E5, по 2 с каждая),
|
||||
`confirm_request("mqs_tone")`, оператор слышит → PASS/FAIL.
|
||||
`critical=false`, `requires_hil=false`, `pre_confirm_prompt=NULL`.
|
||||
- **MQS порядок init:** `bsp_mqs_amp_init()` → `bsp_delay(300)` → `bsp_mqs_init()`.
|
||||
Усилитель запускается первым, чтобы успели зарядиться конденсаторы C103/C105 LM4875M.
|
||||
Нарушение порядка приводит к щелчку при старте или отсутствию звука.
|
||||
- **MQS USB keepalive:** воспроизведение через `bsp_mqs_play()` (async, не blocking),
|
||||
параллельно крутится `bsp_usb_cdc_poll()`. Blocking-вариант голодает USB за ~4 с.
|
||||
- **`pwmchannelenable` (NXP SDK ≥ 2.13):** поле в `pwm_signal_param_t` обязательно
|
||||
выставлять в `true`. При инициализации через designated initializers без явного
|
||||
указания равно `false` → `PWM_SetupPwm()` не выставляет `OUTEN` → ШИМ не выходит
|
||||
на пин. Маскируется после отладочной сессии (отладчик оставляет `OUTEN` от прошлого
|
||||
прогона). Воспроизводится только при cold reset.
|
||||
- **ERRATA 50235 (FlexCAN + USB):** `FLEXCAN_Init()` содержит assert на
|
||||
`CCM_CCGR5_CG12` (LPUART clock gate). После `bsp_usb_cdc_init()` gate
|
||||
может быть закрыт → assert → HardFault. Workaround: `CLOCK_EnableClock(kCLOCK_Lpuart1)`
|
||||
перед `FLEXCAN_Init()` внутри `bsp_can_init()`. Gate оставляется открытым —
|
||||
закрывать не нужно, LPUART1 тактируется с минимальным потреблением.
|
||||
`bsp_can_init()` вызывается из `main()` после `bsp_usb_cdc_init()`.
|
||||
- **`firmware_cdc` фикстура:** не ждёт `session_start` (одноразовое событие при
|
||||
старте, может быть пропущено). Проверяет живость через `ping → pong`.
|
||||
- **`bsp_opto_force_read()`:** добавлен в BSP API для синхронного чтения пина
|
||||
без дебаунса. Обновляет `confirmed_state`, сбрасывает `pending`. Используется
|
||||
в `test_opto.c` после settle — обходит race condition когда чётное число ISR
|
||||
при дребезге реле оставляет `pending=false` с устаревшим `confirmed_state`.
|
||||
- **`bsp_opto_process()` в `test_opto.c`:** вызывается в settle loop после confirm,
|
||||
но не в `test_runner_wait_confirm()`. Финальное чтение — через `bsp_opto_force_read()`.
|
||||
- **Оркестратор оpto:** `RELAY_ON_S=0.15`, `RELAY_OFF_S=0.5` в `06_test_firmware_opto.py`.
|
||||
Фиксированный sleep достаточен — реле переключается до отправки `confirmed:true`,
|
||||
`bsp_opto_force_read()` читает финальное состояние пина напрямую.
|
||||
|
||||
### Этап 8 (TUI решения)
|
||||
|
||||
- **Прошивка — только USB SDP:** SWD недоступен сервиснику. spsdk (sdphost + blhost).
|
||||
Оператор сам переставляет перемычку BOOT — это ок, документируется.
|
||||
- **TUI автодетект:** определяет подключение по VID/PID — SDP BootROM (1FC9:0130)
|
||||
или CDC firmware_test (session_start) — и показывает соответствующий экран.
|
||||
- **Фреймворк TUI:** Textual (Python). Нативный async, реальные виджеты,
|
||||
работает в SSH-сессии, вписывается в uv-экосистему.
|
||||
- **tools/shared/m5_agent.py:** общая M5-логика, импортируется из `tools/hil/`
|
||||
и `tools/production/`.
|
||||
|
||||
---
|
||||
|
||||
## Этап 6 — test_can + test_opto + test_mqs + протокол ✅ ЗАВЕРШЁН
|
||||
|
||||
### 6а — Расширение протокола ✅
|
||||
|
||||
**Файлы:** `protocol.h`, `protocol.c`, `cli.c`, `test_runner.c`, `PROTOCOL.md`
|
||||
|
||||
#### Новая команда `list_tests`
|
||||
|
||||
```json
|
||||
→ {"type":"cmd","cmd":"list_tests"}
|
||||
← {"type":"test_list","tests":[
|
||||
{"id":"sdram","name":"SDRAM 32 MB","critical":true,"requires_hil":false},
|
||||
{"id":"qspi","name":"QSPI Flash W25Qxx","critical":true,"requires_hil":false},
|
||||
{"id":"usd","name":"microSD (SDIO)","critical":false,"requires_hil":false},
|
||||
{"id":"display","name":"TFT Display RGB888","critical":false,"requires_hil":false},
|
||||
{"id":"buttons","name":"Test Buttons","critical":false,"requires_hil":false},
|
||||
{"id":"mqs","name":"MQS Audio Out","critical":false,"requires_hil":false},
|
||||
{"id":"can","name":"CAN loopback","critical":false,"requires_hil":true},
|
||||
{"id":"opto","name":"Opto Inputs","critical":false,"requires_hil":true}
|
||||
]}
|
||||
```
|
||||
|
||||
#### Новая команда `run_selected`
|
||||
|
||||
```json
|
||||
→ {"type":"cmd","cmd":"run_selected","tests":["sdram","qspi","display"]}
|
||||
← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
|
||||
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
|
||||
← {"type":"test_begin","id":"qspi",...}
|
||||
← {"type":"test_result","id":"qspi",...}
|
||||
← {"type":"test_begin","id":"display",...}
|
||||
← {"type":"test_result","id":"display",...}
|
||||
← {"type":"summary","passed":3,"failed":0,"skipped":0,"overall":"pass"}
|
||||
```
|
||||
|
||||
Если хотя бы один ID не найден в реестре:
|
||||
|
||||
```json
|
||||
← {"ok":false,"error":"UNKNOWN_TEST"}
|
||||
```
|
||||
|
||||
**Реализация в `test_runner.c`:**
|
||||
|
||||
- Новый режим `RUNNER_MODE_SELECTED`
|
||||
- Статический bool-массив `g_s_selected[REGISTRY_SIZE]` — без malloc
|
||||
- `test_runner_run_selected(const char **pp_ids, size_t count)` — новая публичная функция
|
||||
|
||||
### 6б — test_opto.c ✅
|
||||
|
||||
**Файл:** `firmware/test/src/tests/test_opto.c`
|
||||
|
||||
6 шагов, попарно ACTIVE/INACTIVE для трёх каналов:
|
||||
|
||||
| Шаг | confirm_request id | M5 действие | Проверка |
|
||||
| --- | ------------------- | ----------- | -------------------------------- |
|
||||
| 1 | `opto_in1_active` | RLY3 ON | `bsp_opto_read(IN1) == ACTIVE` |
|
||||
| 2 | `opto_in1_inactive` | RLY3 OFF | `bsp_opto_read(IN1) == INACTIVE` |
|
||||
| 3 | `opto_in2_active` | RLY4 ON | `bsp_opto_read(IN2) == ACTIVE` |
|
||||
| 4 | `opto_in2_inactive` | RLY4 OFF | `bsp_opto_read(IN2) == INACTIVE` |
|
||||
| 5 | `opto_rs_active` | RLY2 ON | `bsp_opto_read(RS) == ACTIVE` |
|
||||
| 6 | `opto_rs_inactive` | RLY2 OFF | `bsp_opto_read(RS) == INACTIVE` |
|
||||
|
||||
- Init: `bsp_opto_init()` единым вызовом для всех каналов
|
||||
- Верификация синхронная после confirm (M5 переключил реле до отправки `confirmed:true`)
|
||||
- FAIL при несоответствии: `detail = "<id> state mismatch: expected ACTIVE got INACTIVE"`
|
||||
- Таймаут: `PROTOCOL_CONFIRM_TIMEOUT_MS` (30 с) на каждый шаг
|
||||
|
||||
### 6в — test_can.c ✅
|
||||
|
||||
**Файл:** `firmware/test/src/tests/test_can.c`
|
||||
|
||||
2 шага, оба направления независимо:
|
||||
|
||||
**Шаг 1 — RX (M5 → таргет):**
|
||||
|
||||
```bash
|
||||
confirm_request("can_rx_ready")
|
||||
→ TUI: M5.can_send(id=0x100, data=[0xDE,0xAD,0xBE,0xEF])
|
||||
→ TUI: confirm(true)
|
||||
→ таргет: bsp_can_receive(&frame, 500 мс)
|
||||
→ верификация: frame.id==0x100, frame.data==[0xDE,0xAD,0xBE,0xEF]
|
||||
→ FAIL если timeout или несовпадение
|
||||
```
|
||||
|
||||
**Шаг 2 — TX (таргет → M5):**
|
||||
|
||||
```bash
|
||||
bsp_can_send(id=0x200, data=[0xCA,0xFE,0xBA,0xBE], timeout=100 мс)
|
||||
confirm_request("can_tx_verify")
|
||||
→ TUI: M5.can_recv(timeout=500 мс) → верификация id+data
|
||||
→ TUI: confirm(true) если M5 принял корректно, confirm(false) если нет
|
||||
→ FAIL если confirmed=false или timeout
|
||||
```
|
||||
|
||||
- `disableSelfReception=true` — таргет не слышит свой TX, только M5 верифицирует
|
||||
- Init: `bsp_can_init(&cfg)` + `bsp_can_accept_all()`
|
||||
|
||||
### 6г — bsp_mqs + test_mqs.c ✅
|
||||
|
||||
**Файлы:** `bsp/mqs/` + `firmware/test/src/tests/test_mqs.c`
|
||||
|
||||
**bsp_mqs:**
|
||||
|
||||
- SAI3 + eDMA (DMA0 канал 0) + MQS периферия
|
||||
- Стерео PCM16 буфер (L+R идентичны), один физический выход `MQS_RIGHT`
|
||||
- Усилитель LM4875M управляется PWM4 SM0 через RC-фильтр и буферный ОУ LM358
|
||||
- API: `bsp_mqs_init/deinit`, `bsp_mqs_play/play_blocking`, `bsp_mqs_stop`,
|
||||
`bsp_mqs_is_busy`, `bsp_mqs_amp_init/deinit`, `bsp_mqs_amp_set_volume`
|
||||
|
||||
**test_mqs:**
|
||||
|
||||
- Мелодия ~4 с: A4 (440 Гц) + E5 (659 Гц), по 2 с каждая, целочисленная LUT-синусоида
|
||||
- Воспроизведение через `bsp_mqs_play()` (async) с `bsp_usb_cdc_poll()` в цикле
|
||||
- `confirm_request("mqs_tone", "Do you hear a tone?", 15000)` → PASS/FAIL
|
||||
- Порядок init: amp → delay 300 мс → mqs → build_melody (однократно, флаг)
|
||||
- `critical=false`, `requires_hil=false`, `pre_confirm_prompt=NULL`
|
||||
|
||||
### 6д — HIL pytest для firmware_test ✅
|
||||
|
||||
**Файлы:**
|
||||
|
||||
```
|
||||
tools/hil/conftest.py ← фикстура firmware_cdc
|
||||
tools/hil/06_test_firmware_opto.py
|
||||
tools/hil/06_test_firmware_can.py
|
||||
```
|
||||
|
||||
**Фикстура `firmware_cdc`:**
|
||||
|
||||
```python
|
||||
@pytest.fixture(scope="module")
|
||||
def firmware_cdc(m5):
|
||||
"""
|
||||
Открывает USB CDC порт firmware_test.
|
||||
firmware_test уже прошит в Flash (не загружается pyOCD).
|
||||
Проверяет живость через ping → pong.
|
||||
"""
|
||||
```
|
||||
|
||||
**`FirmwareCdcClient`** — тонкий клиент:
|
||||
|
||||
- `send_cmd(cmd_dict)` — отправить JSON команду
|
||||
- `wait_event(type, timeout_s)` — ждать события нужного типа
|
||||
- `confirm(id, ok)` — отправить `{"type":"confirm","id":"...","confirmed":true/false}`
|
||||
- `run_test(id)` — запустить тест, вернуть test_result dict
|
||||
|
||||
**Justfile:**
|
||||
|
||||
```bash
|
||||
hil-firmware-opto → pytest 06_test_firmware_opto.py -v
|
||||
hil-firmware-can → pytest 06_test_firmware_can.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Этап 7 — Provisioning
|
||||
|
||||
### Что нужно
|
||||
|
||||
1. Читать `OCOTP_UNIQUE_ID` через SDK `fsl_ocotp`
|
||||
2. Отправить `{"type":"provision_ready","chip_uid":"AABB..."}` после `summary`
|
||||
3. Ждать `{"type":"cmd","cmd":"provision_ack"}` от хоста
|
||||
4. Записывать статус в Flash (первый сектор после прошивки, вне XIP)
|
||||
|
||||
### BSP (предварительно)
|
||||
|
||||
```c
|
||||
/* bsp/provisioning/include/bsp/provisioning.h */
|
||||
bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); /* 8 байт из OCOTP */
|
||||
```
|
||||
|
||||
### Открытые вопросы — Этап 7
|
||||
|
||||
- [ ] Что именно записывать как «пройдено»: флаг в Flash или только отправить UID?
|
||||
- [ ] Нужна ли защита от повторного provisioning (write-once)?
|
||||
|
||||
---
|
||||
|
||||
## Этап 8 — TUI сервисного инженера
|
||||
|
||||
### Стек технологий
|
||||
|
||||
| Компонент | Выбор | Обоснование |
|
||||
| ------------- | ------------- | ----------------------------------------------------- |
|
||||
| TUI фреймворк | **Textual** | Нативный async, виджеты, SSH-совместим, uv-экосистема |
|
||||
| Serial | pyserial | Уже в стеке (tools/hil) |
|
||||
| Прошивка | spsdk | sdphost + blhost, уже в tools/host |
|
||||
| Конфигурация | python-dotenv | .env файл, совместим с существующим подходом |
|
||||
|
||||
### Структура приложения
|
||||
|
||||
```bash
|
||||
tools/production/
|
||||
├── pyproject.toml ← зависимости: textual, pyserial, spsdk, python-dotenv
|
||||
├── uv.lock
|
||||
├── main.py ← точка входа
|
||||
├── app/
|
||||
│ ├── tui.py ← Textual App, экраны, layout
|
||||
│ ├── firmware_client.py ← USB CDC asyncio клиент firmware_test
|
||||
│ ├── m5_client.py ← M5 Serial клиент (импортирует tools/shared/m5_agent.py)
|
||||
│ ├── flasher.py ← USB SDP обёртка над spsdk
|
||||
│ ├── orchestrator.py ← confirm_request → M5 action → confirm response
|
||||
│ └── models.py ← TestInfo, TestResult, SessionState (dataclasses)
|
||||
└── README.md
|
||||
|
||||
tools/shared/
|
||||
└── m5_agent.py ← общая M5-логика для hil/ и production/
|
||||
```
|
||||
|
||||
### Два режима работы
|
||||
|
||||
**Режим A — Прошивка** (триггер: VID/PID 1FC9:0130 обнаружен — BootROM SDP)
|
||||
|
||||
```
|
||||
┌─ Прошивка платы ─────────────────────────────────┐
|
||||
│ Обнаружен BootROM (SDP режим) │
|
||||
│ │
|
||||
│ Что прошить? │
|
||||
│ ◉ firmware_test (диагностика) │
|
||||
│ ○ Production (bootloader + tft_app) │
|
||||
│ │
|
||||
│ Файл: [/path/to/firmware_test_hab.bin ···] │
|
||||
│ │
|
||||
│ [ Прошить ] │
|
||||
│ │
|
||||
│ ████████████░░░░░░ 64% Запись во Flash... │
|
||||
└────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Режим B — Диагностика** (триггер: session_start получен по CDC)
|
||||
|
||||
```
|
||||
┌─ Диагностика платы fw:0.1.0 ─────────────────────┐
|
||||
│ M5StampPLC: ✓ подключён │ Плата: IMXRT1052 │
|
||||
├────────────────────────────────────────────────────┤
|
||||
│ Выбор тестов: │ Результаты: │
|
||||
│ ☑ SDRAM 32 MB │ sdram ✓ PASS │
|
||||
│ ☑ QSPI Flash │ qspi ✓ PASS │
|
||||
│ ☑ microSD │ usd ✗ FAIL │
|
||||
│ ☑ TFT Display │ mount failed: 5 │
|
||||
│ ☑ Кнопки │ display ✓ PASS │
|
||||
│ ☑ MQS Audio │ buttons ✓ PASS │
|
||||
│ ☑ CAN loopback [HIL] │ mqs ✓ PASS │
|
||||
│ ☑ Оптовходы [HIL] │ ... │
|
||||
├────────────────────────────────────────────────────┤
|
||||
│ [ Запустить выбранные ] [ Все тесты ] │
|
||||
│ ████████████████░░░░ 80% Тест: display │
|
||||
├────────────────────────────────────────────────────┤
|
||||
│ ⚠ Экран залит красным цветом? │
|
||||
│ [ ✓ Да ] [ ✗ Нет ] │
|
||||
└────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Поведение confirm_request в TUI
|
||||
|
||||
| Тип теста | Источник confirm | Действие TUI |
|
||||
| -------------------- | ------------------ | --------------------------------------------- |
|
||||
| standalone (display) | оператор | показать prompt, кнопки OK/FAIL, countdown |
|
||||
| standalone (mqs) | оператор | показать prompt, кнопки OK/FAIL, countdown |
|
||||
| standalone (buttons) | физическое нажатие | показать инструкцию, ждать test_result |
|
||||
| HIL (opto, can) | оркестратор | auto: M5 action → confirm (оператор не видит) |
|
||||
|
||||
HIL confirm полностью автоматический — оператор видит только прогресс, не интерактивный prompt.
|
||||
|
||||
### Конфигурация (.env)
|
||||
|
||||
```ini
|
||||
# Существующие переменные (tools/hil/.env):
|
||||
HIL_VCOM_PORT=/dev/ttyACM0
|
||||
HIL_M5_PORT=/dev/ttyACM1
|
||||
|
||||
# Новые переменные для production TUI:
|
||||
SERVICE_CDC_PORT=AUTO # AUTO = автодетект по session_start
|
||||
SERVICE_M5_PORT=AUTO # AUTO = автодетект, пусто = без M5
|
||||
FIRMWARE_TEST_BIN=build/Release/firmware_test_hab.bin
|
||||
PRODUCTION_BIN_BOOT=build/Release/bootloader_hab.bin
|
||||
PRODUCTION_BIN_APP=build/Release/tft_app_hab.bin
|
||||
```
|
||||
|
||||
### Запуск
|
||||
|
||||
```bash
|
||||
just host::service-tui # запустить TUI сервисного инженера
|
||||
just host::service-flash <bin> # прошить без TUI (для автоматизации)
|
||||
```
|
||||
|
||||
### Процесс работы сервисника
|
||||
|
||||
**Диагностика (firmware_test уже в Flash):**
|
||||
|
||||
```bash
|
||||
1. Плата в нормальном режиме (BOOT_MOD_1 → GND)
|
||||
2. Подключить USB к сервисному ПК
|
||||
3. just host::service-tui → TUI обнаружил session_start → Режим B
|
||||
4. Выбрать тесты → Запустить → Смотреть результаты
|
||||
```
|
||||
|
||||
**Перепрошивка (нужна новая версия firmware_test или production):**
|
||||
|
||||
```bash
|
||||
1. Перемычка BOOT_MOD_1 → 3V3
|
||||
2. Reset, подключить USB
|
||||
3. TUI обнаружил 1FC9:0130 → Режим A
|
||||
4. Выбрать бинарь → Прошить
|
||||
5. Перемычка BOOT_MOD_1 → GND → Reset → TUI переходит в Режим B
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Порядок реализации
|
||||
|
||||
```
|
||||
✅ Этап 1 протокол v2 + runner
|
||||
✅ Этап 2 bsp_sdram + test_sdram
|
||||
✅ Этап 3 bsp_qspi_flash + test_qspi
|
||||
✅ Этап 4 bsp_sd + test_usd
|
||||
✅ Этап 5 display + buttons
|
||||
✅ Этап 6а протокол: list_tests + run_selected
|
||||
✅ Этап 6б test_opto.c + hardware верификация
|
||||
✅ Этап 6в test_can.c + hardware верификация
|
||||
✅ Этап 6д HIL pytest: firmware_cdc фикстура (FirmwareCdc + firmware_cdc)
|
||||
✅ Этап 6е HIL pytest: 06_test_firmware_opto.py
|
||||
✅ Этап 6ж HIL pytest: 06_test_firmware_can.py
|
||||
✅ Этап 6г bsp_mqs + test_mqs.c + hardware верификация
|
||||
|
||||
⬜ Этап 7 Provisioning (OCOTP UID + Flash-флаг) ← СЛЕДУЮЩИЙ ШАГ
|
||||
|
||||
⬜ Этап 8а tools/production/ скелет + models + clients
|
||||
⬜ Этап 8б orchestrator + базовый Textual UI (список тестов, запуск, результаты)
|
||||
⬜ Этап 8в Экран прошивки (flasher + SDP автодетект)
|
||||
⬜ Этап 8г Provisioning в TUI
|
||||
⬜ Этап 8д tools/shared/m5_agent.py (рефакторинг общей M5-логики)
|
||||
|
||||
⬜ Этап 9 Параллельно: обновить README + DEV_ARCH.md под финальную архитектуру
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Зависимости между этапами
|
||||
|
||||
```
|
||||
✅ 6а (протокол) → ✅ 6б (opto) → ✅ 6в (can) → ✅ 6г (mqs)
|
||||
↓
|
||||
✅ 6д (conftest) → ✅ 6е (opto pytest) → ✅ 6ж (can pytest)
|
||||
↓
|
||||
⬜ 7 (provisioning)
|
||||
↓
|
||||
⬜ 8 (TUI)
|
||||
```
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
# firmware_test
|
||||
|
||||
> Диагностическая прошивка для плат **MIMXRT1052CVJ5B**, вернувшихся по
|
||||
> рекламации. Запускается сервисным инженером через USB CDC ACM без
|
||||
> предварительной прошивки загрузчика.
|
||||
> Диагностическая прошивка для плат TFT индикаторов, вернувшихся по
|
||||
> рекламации. Загружается на таргет сервисным инженером через USB (через BootROM IMXRT1052).
|
||||
>
|
||||
> Версия прошивки: `0.1.0` | Протокол: v2
|
||||
>Версия прошивки: `0.1.2` | Протокол: v2
|
||||
>
|
||||
>Версия — из `project(firmware_test VERSION X.Y.Z)` в `CMakeLists.txt`
|
||||
> (см. [Версионирование](#версионирование))
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -45,7 +47,7 @@ just build::hab-firmware-test-debug
|
|||
# Перевести плату в SDP-режим: BOOT_MOD_1 → 3V3 → Reset
|
||||
just host::flash-test-debug
|
||||
|
||||
# Или через SWD (power cycle после)
|
||||
# Или через SWD
|
||||
just host::flash-swd-test-debug
|
||||
```
|
||||
|
||||
|
|
@ -66,7 +68,7 @@ screen /dev/ttyACM0
|
|||
После подключения таргет сразу присылает:
|
||||
|
||||
```json
|
||||
{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
|
||||
{"type":"session_start","fw":"0.1.2","target":"IMXRT1052","uptime_ms":0}
|
||||
```
|
||||
|
||||
Проверка связи:
|
||||
|
|
@ -81,7 +83,7 @@ screen /dev/ttyACM0
|
|||
```json
|
||||
→ {"type":"cmd","cmd":"run","id":"sdram"}
|
||||
← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
|
||||
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
|
||||
← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""}
|
||||
```
|
||||
|
||||
Запуск всех тестов:
|
||||
|
|
@ -110,12 +112,12 @@ just build::test-host
|
|||
│ JSON-lines, 1 строка = 1 сообщение
|
||||
▼
|
||||
[Плата MIMXRT1052 с firmware_test]
|
||||
│ GPIO / LPUART / SEMC / FlexSPI / USDHC
|
||||
│ GPIO / LPUART / SEMC / FlexSPI / USDHC / SAI(MQS)
|
||||
▼
|
||||
[Периферия: SDRAM, QSPI Flash, uSD, Display, Кнопки, CAN, UART, Opto]
|
||||
[Периферия: SDRAM, QSPI Flash, uSD, Display, Кнопки, CAN, MQS, Opto]
|
||||
▲
|
||||
[M5StampPLC — управление внешними сигналами для HIL тестов]
|
||||
(реле → EXT_IN1/IN2, RS_RX, CAN, UART echo)
|
||||
(реле → EXT_IN1/IN2, RS_RX; CAN loopback)
|
||||
```
|
||||
|
||||
**Принцип разделения ответственности:**
|
||||
|
|
@ -123,8 +125,8 @@ just build::test-host
|
|||
- Вся тест-логика живёт **на таргете** (`test_runner.c`, `tests/*.c`).
|
||||
- Хост — тонкий клиент: отправляет команды, отображает события, управляет
|
||||
интерактивными шагами через `confirm`.
|
||||
- Тесты **атомарны**: инженер запускает один тест или все сразу — порядок
|
||||
не фиксирован.
|
||||
- Тесты **атомарны**: инженер запускает один тест, подмножество или все сразу —
|
||||
порядок не фиксирован.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -136,6 +138,8 @@ firmware/test/
|
|||
└── src/
|
||||
├── main.c — инициализация BSP, главный цикл
|
||||
│
|
||||
├── version.h.in — шаблон версии (CMake → generated/version.h)
|
||||
│
|
||||
├── cli.h / cli.c — IO-слой
|
||||
│ буферизация строк, парсинг "type",
|
||||
│ диспатч на test_runner / protocol
|
||||
|
|
@ -149,20 +153,23 @@ firmware/test/
|
|||
│
|
||||
├── test_runner.h / .c — реестр + state machine
|
||||
│ IDLE → PRE_CONFIRM → RUNNING → IDLE
|
||||
│ test_runner_wait_confirm() для display
|
||||
│ test_runner_wait_confirm() для in-run confirm
|
||||
│
|
||||
└── tests/
|
||||
├── test_sdram.c — SDRAM 32 MB (self)
|
||||
├── test_qspi.c — QSPI Flash 8 MB (self)
|
||||
├── test_usd.c — uSD SDIO (interactive)
|
||||
├── test_display.c — Display RGB888 (interactive)
|
||||
├── test_buttons.c — Test_But_1/2 (interactive)
|
||||
├── test_qspi.c — QSPI Flash W25Qxx (self)
|
||||
├── test_usd.c — microSD SDIO (interactive, pre-confirm)
|
||||
├── test_display.c — Display RGB888 (interactive, in-run confirm)
|
||||
├── test_buttons.c — Test_But_1/2 (interactive, физическое нажатие)
|
||||
├── test_opto.c — Opto-in EXT_IN1/IN2 + RS_RX (HIL)
|
||||
├── test_can.c — CAN loopback (HIL)
|
||||
├── test_uart_ttl.c — UART TTL (HIL)
|
||||
├── test_uart_iso.c — UART ISO / RS_RX Opto (HIL)
|
||||
└── test_opto.c — Opto-in EXT_IN1/IN2 (HIL)
|
||||
└── test_mqs.c — MQS Audio Out (interactive, in-run confirm)
|
||||
```
|
||||
|
||||
> Порядок файлов в `tests/` — как в `CMakeLists.txt`. Порядок **выполнения**
|
||||
> тестов определяется реестром `k_registry[]` в `test_runner.c`
|
||||
> (см. [Матрица тестов](#матрица-тестов)).
|
||||
|
||||
---
|
||||
|
||||
### Граф зависимостей
|
||||
|
|
@ -175,8 +182,9 @@ main.c
|
|||
├── bsp_usb_cdc (USB CDC ACM, единственный транспорт)
|
||||
├── cli.c
|
||||
│ └── bsp_usb_cdc (read / write)
|
||||
│ └── protocol.c (send_error, send_pong)
|
||||
│ └── test_runner.c (run_single, run_all, on_confirm)
|
||||
│ └── bsp_provisioning (bsp_prov_read_uid — для get_uid)
|
||||
│ └── protocol.c (send_error, send_pong, send_uid/version_response)
|
||||
│ └── test_runner.c (run_single, run_all, run_selected, send_list, on_confirm)
|
||||
├── protocol.c
|
||||
│ └── cli.c (cli_send)
|
||||
│ └── bsp_tick (bsp_tick_get_ms — для uptime)
|
||||
|
|
@ -188,26 +196,28 @@ main.c
|
|||
└── tests/*.c (тест-модули через реестр)
|
||||
```
|
||||
|
||||
**BSP-зависимости тест-модулей:**
|
||||
**BSP-зависимости тест-модулей** (по `target_link_libraries` в `CMakeLists.txt`):
|
||||
|
||||
| Тест | BSP модуль |
|
||||
| --------------- | ------------------------------ |
|
||||
| `test_sdram` | `bsp_sdram` |
|
||||
| `test_qspi` | `bsp_qspi` |
|
||||
| `test_usd` | `bsp_usd` |
|
||||
| `test_display` | существующий display BSP |
|
||||
| `test_buttons` | `bsp_button` ✅ |
|
||||
| `test_can` | `bsp_can` ✅ |
|
||||
| `test_uart_ttl` | `bsp_uart_host` ✅ |
|
||||
| `test_uart_iso` | `bsp_opto` (rs_as_gpio=true) ✅ |
|
||||
| `test_opto` | `bsp_opto` ✅ |
|
||||
| Тест | BSP модуль |
|
||||
| -------------- | ---------------------------------- |
|
||||
| `test_sdram` | `bsp_sdram` |
|
||||
| `test_qspi` | `bsp_qspi_flash` |
|
||||
| `test_usd` | `bsp_sd` (+ `firmware_test_fatfs`) |
|
||||
| `test_display` | `bsp_display` |
|
||||
| `test_buttons` | `bsp_button` |
|
||||
| `test_opto` | `bsp_opto` (rs_as_gpio=true) |
|
||||
| `test_can` | `bsp_can` |
|
||||
| `test_mqs` | `bsp_mqs` |
|
||||
|
||||
> `bsp_uart_host` также линкуется (используется вне тест-реестра); отдельного
|
||||
> UART-тест-модуля в текущем реестре нет (тестируется в `tests/target`).
|
||||
|
||||
---
|
||||
|
||||
### State machine test_runner
|
||||
|
||||
```bash
|
||||
cmd: run / run_all
|
||||
cmd: run / run_all / run_selected
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
|
|
@ -232,9 +242,9 @@ main.c
|
|||
confirmed=false → SKIP │ │
|
||||
timeout → SKIP │ │
|
||||
│ │
|
||||
run: IDLE ──┘ │
|
||||
run_all: следующий тест в реестре ───┘
|
||||
run_all done: protocol_send_summary()
|
||||
run: IDLE ──┘ │
|
||||
run_all / run_selected: следующий тест ───┘
|
||||
done: protocol_send_summary()
|
||||
```
|
||||
|
||||
**Ключевые свойства state machine:**
|
||||
|
|
@ -242,10 +252,12 @@ main.c
|
|||
- `RUNNING` — защита от ложного `is_busy()==false` во время blocking `run()`.
|
||||
Пока тест выполняется, новые команды получают `BUSY`.
|
||||
- `test_runner_wait_confirm()` — вызывается из `run()` интерактивных тестов
|
||||
(display). Внутри polling loop: `bsp_usb_cdc_poll()` + `cli_process()`.
|
||||
(display, mqs). Внутри polling loop: `bsp_usb_cdc_poll()` + `cli_process()`.
|
||||
USB-стек остаётся живым, confirm приходит без возврата в главный цикл.
|
||||
- `critical=true` + `FAIL` в `run_all` → все оставшиеся тесты получают
|
||||
`SKIP` немедленно, `summary.overall = "fail"`.
|
||||
- `run_selected` работает по той же машине над маской выбранных тестов
|
||||
(`g_s_selected[]`); порядок — по реестру, не по порядку в запросе.
|
||||
- `critical=true` + `FAIL` в `run_all`/`run_selected` → все оставшиеся тесты
|
||||
получают `SKIP` немедленно, `summary.overall = "fail"`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -258,7 +270,7 @@ main.c
|
|||
| Интерфейс | USB CDC ACM, разъём J2 |
|
||||
| Кодировка | UTF-8 |
|
||||
| Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` |
|
||||
| Максимальная длина строки | 128 байт включая `\n` |
|
||||
| Максимальная длина строки | 128 байт включая `\n` (`CLI_LINE_BUF_SIZE`) |
|
||||
| CR+LF | Принимается (таргет отбрасывает `\r`) |
|
||||
|
||||
Нет хэндшейка, нет sequence number, нет подтверждений доставки.
|
||||
|
|
@ -273,7 +285,7 @@ main.c
|
|||
│ │
|
||||
│ [USB SDP: прошивка загружена] │
|
||||
│ [CDC ACM: порт открыт] │
|
||||
│◄─── {"type":"session_start","fw":"0.1.0",...} │ автоматически
|
||||
│◄─── {"type":"session_start","fw":"0.1.2",...} │ автоматически
|
||||
│ │
|
||||
│──── {"type":"cmd","cmd":"ping"} ─────────────►│
|
||||
│◄─── {"type":"pong"} │
|
||||
|
|
@ -290,7 +302,8 @@ main.c
|
|||
```
|
||||
|
||||
`session_start` отправляется **автоматически** при каждом старте, до получения
|
||||
первой команды. Хост должен быть готов принять его сразу после открытия порта.
|
||||
первой команды. Хост должен быть готов принять его сразу после открытия порта
|
||||
(либо не полагаться на него — фикстуры HIL проверяют живость через `ping`).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -310,7 +323,7 @@ main.c
|
|||
```json
|
||||
→ {"type":"cmd","cmd":"run","id":"sdram"}
|
||||
← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
|
||||
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
|
||||
← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""}
|
||||
```
|
||||
|
||||
Если `id` не найден:
|
||||
|
|
@ -324,13 +337,58 @@ main.c
|
|||
```json
|
||||
→ {"type":"cmd","cmd":"run_all"}
|
||||
← {"type":"test_begin","id":"sdram",...}
|
||||
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
|
||||
← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""}
|
||||
← {"type":"test_begin","id":"qspi",...}
|
||||
← {"type":"test_result","id":"qspi","status":"pass","ms":88,"detail":""}
|
||||
← {"type":"test_result","id":"qspi","status":"pass","ms":86,"detail":""}
|
||||
← ... (остальные тесты) ...
|
||||
← {"type":"summary","passed":7,"failed":0,"skipped":1,"overall":"pass"}
|
||||
```
|
||||
|
||||
#### `run_selected` — запуск подмножества тестов
|
||||
|
||||
```json
|
||||
→ {"type":"cmd","cmd":"run_selected","tests":["sdram","opto"]}
|
||||
← {"type":"test_begin","id":"sdram",...}
|
||||
← {"type":"test_result","id":"sdram","status":"pass",...}
|
||||
← {"type":"test_begin","id":"opto",...}
|
||||
← {"type":"test_result","id":"opto","status":"pass",...}
|
||||
← {"type":"summary","passed":2,"failed":0,"skipped":0,"overall":"pass"}
|
||||
```
|
||||
|
||||
Порядок выполнения — по реестру таргета, не по порядку в запросе. Если хотя
|
||||
бы один ID не найден — вся команда отклоняется (`UNKNOWN_TEST`), не запускается
|
||||
ничего.
|
||||
|
||||
#### `list_tests` — получить реестр тестов
|
||||
|
||||
```json
|
||||
→ {"type":"cmd","cmd":"list_tests"}
|
||||
← {"type":"test_list","tests":[
|
||||
{"id":"sdram","name":"SDRAM 32 MB","critical":true,"requires_hil":false},
|
||||
... остальные ...
|
||||
]}
|
||||
```
|
||||
|
||||
Хост (TUI) использует ответ для динамического построения списка тестов;
|
||||
`requires_hil=true` тесты недоступны при отсутствии M5StampPLC.
|
||||
|
||||
#### `get_uid` — прочитать UID чипа
|
||||
|
||||
```json
|
||||
→ {"type":"cmd","cmd":"get_uid"}
|
||||
← {"type":"uid_response","uid":"A1B2C3D4E5F60011"}
|
||||
```
|
||||
|
||||
`uid` — 8 байт (`BSP_PROV_UID_LEN`) big-endian, 16 hex-символов без
|
||||
разделителей. При ошибке чтения: `{"ok":false,"error":"UID_READ_ERR"}`.
|
||||
|
||||
#### `get_version` — прочитать версию прошивки
|
||||
|
||||
```json
|
||||
→ {"type":"cmd","cmd":"get_version"}
|
||||
← {"type":"version_response","fw":"0.1.2"}
|
||||
```
|
||||
|
||||
#### `confirm` — ответ оператора на интерактивный шаг
|
||||
|
||||
```json
|
||||
|
|
@ -347,35 +405,19 @@ main.c
|
|||
#### `session_start`
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "session_start",
|
||||
"fw": "0.1.0",
|
||||
"target": "IMXRT1052",
|
||||
"uptime_ms": 0
|
||||
}
|
||||
{ "type":"session_start", "fw":"0.1.2", "target":"IMXRT1052", "uptime_ms":0 }
|
||||
```
|
||||
|
||||
#### `test_begin`
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "test_begin",
|
||||
"id": "sdram",
|
||||
"name": "SDRAM 32 MB",
|
||||
"critical": true
|
||||
}
|
||||
{ "type":"test_begin", "id":"sdram", "name":"SDRAM 32 MB", "critical":true }
|
||||
```
|
||||
|
||||
#### `test_result`
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "test_result",
|
||||
"id": "sdram",
|
||||
"status": "pass",
|
||||
"ms": 312,
|
||||
"detail": ""
|
||||
}
|
||||
{ "type":"test_result", "id":"sdram", "status":"pass", "ms":15304, "detail":"" }
|
||||
```
|
||||
|
||||
| `status` | Смысл |
|
||||
|
|
@ -384,37 +426,39 @@ main.c
|
|||
| `"fail"` | Тест провален; `detail` содержит описание (до 95 символов) |
|
||||
| `"skip"` | Пропущен: нет оборудования, таймаут, отказ оператора, critical fail выше |
|
||||
|
||||
Примеры `detail`: `"addr=0x80001000 expected=0xA5 got=0x00"`, `"JEDEC ID mismatch"`.
|
||||
Примеры `detail`: `"addr=0x80200001 exp=0x02 got=0xFF"`, `"JEDEC: mfr=0xFF exp=0xEF"`.
|
||||
|
||||
#### `test_list`
|
||||
|
||||
Ответ на `list_tests` — массив дескрипторов (`id`, `name`, `critical`,
|
||||
`requires_hil`).
|
||||
|
||||
#### `confirm_request`
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "confirm_request",
|
||||
"id": "display_red",
|
||||
"prompt": "Экран залит красным цветом?",
|
||||
"timeout_ms": 15000
|
||||
}
|
||||
{ "type":"confirm_request", "id":"display_red", "prompt":"Screen is solid red?", "timeout_ms":15000 }
|
||||
```
|
||||
|
||||
Хост отображает `prompt` оператору. Авторитетный таймаут — на таргете.
|
||||
Хост может дублировать countdown для UX.
|
||||
|
||||
#### `uid_response` / `version_response`
|
||||
|
||||
Ответы на `get_uid` / `get_version` (см. соответствующие команды выше).
|
||||
|
||||
#### `summary`
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "summary",
|
||||
"passed": 6,
|
||||
"failed": 1,
|
||||
"skipped": 0,
|
||||
"overall": "fail"
|
||||
}
|
||||
{ "type":"summary", "passed":6, "failed":1, "skipped":0, "overall":"fail" }
|
||||
```
|
||||
|
||||
`"overall":"fail"` — если хотя бы один `critical` тест провален.
|
||||
`"overall":"pass"` — все `critical` тесты прошли (non-critical могут fail).
|
||||
|
||||
#### `pong`
|
||||
|
||||
Ответ на `ping`: `{"type":"pong"}`.
|
||||
|
||||
---
|
||||
|
||||
### Ошибки протокола
|
||||
|
|
@ -425,43 +469,60 @@ main.c
|
|||
← {"ok":false,"error":"UNKNOWN_TEST"} — "id" не найден в реестре
|
||||
← {"ok":false,"error":"LINE_TOO_LONG"} — строка превысила 128 байт
|
||||
← {"ok":false,"error":"BUSY"} — таргет выполняет тест
|
||||
← {"ok":false,"error":"UID_READ_ERR"} — bsp_prov_read_uid() вернул ошибку
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Интерактивные тесты
|
||||
|
||||
#### uSD — вставить карту
|
||||
#### microSD — вставить карту (pre-confirm)
|
||||
|
||||
```bash
|
||||
← {"type":"confirm_request","id":"usd_insert","prompt":"Вставьте microSD","timeout_ms":30000}
|
||||
→ {"type":"confirm","id":"usd_insert","confirmed":true}
|
||||
← {"type":"confirm_request","id":"usd","prompt":"Insert microSD card and press OK","timeout_ms":30000}
|
||||
→ {"type":"confirm","id":"usd","confirmed":true}
|
||||
← {"type":"test_begin","id":"usd",...}
|
||||
← {"type":"test_result","id":"usd","status":"pass","ms":541,"detail":""}
|
||||
← {"type":"test_result","id":"usd","status":"pass","ms":874,"detail":""}
|
||||
```
|
||||
|
||||
Если оператор отказался или таймаут:
|
||||
confirm id для pre-confirm равен id теста (`usd`) — механизм `pre_confirm_prompt`
|
||||
использует `mod->id`. Отказ или таймаут 30 с → `SKIP`.
|
||||
|
||||
```bash
|
||||
← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"operator skipped"}
|
||||
```
|
||||
#### Display RGB888 — подтвердить цвета и ротацию (in-run confirm)
|
||||
|
||||
#### Display RGB888 — подтвердить цвета
|
||||
|
||||
Четыре шага R/G/B/W. Итог — AND всех подтверждений.
|
||||
Шесть шагов: Red → Green → Blue → White, затем два ротационных
|
||||
(диагностика непропаянных LR/UD пинов). Тест прерывается на **первом**
|
||||
неподтверждённом шаге (короткое замыкание, не сбор всех ответов).
|
||||
|
||||
```bash
|
||||
← {"type":"test_begin","id":"display",...}
|
||||
← {"type":"confirm_request","id":"display_red","prompt":"Экран красный?","timeout_ms":15000}
|
||||
← {"type":"confirm_request","id":"display_red","prompt":"Screen is solid red?","timeout_ms":15000}
|
||||
→ {"type":"confirm","id":"display_red","confirmed":true}
|
||||
← {"type":"confirm_request","id":"display_green",...}
|
||||
→ {"type":"confirm","id":"display_green","confirmed":true}
|
||||
← {"type":"confirm_request","id":"display_blue",...}
|
||||
→ {"type":"confirm","id":"display_blue","confirmed":true}
|
||||
← {"type":"confirm_request","id":"display_white",...}
|
||||
→ {"type":"confirm","id":"display_white","confirmed":false}
|
||||
← {"type":"test_result","id":"display","status":"fail","ms":22103,
|
||||
"detail":"display_white not confirmed"}
|
||||
→ {"type":"confirm","id":"display_white","confirmed":true}
|
||||
← {"type":"confirm_request","id":"display_rot0","prompt":"Screen: left RED, right BLUE?","timeout_ms":15000}
|
||||
→ {"type":"confirm","id":"display_rot0","confirmed":true}
|
||||
← {"type":"confirm_request","id":"display_rot_base","prompt":"Left RED and right BLUE swapped sides?","timeout_ms":15000}
|
||||
→ {"type":"confirm","id":"display_rot_base","confirmed":true}
|
||||
← {"type":"test_result","id":"display","status":"pass",...}
|
||||
```
|
||||
|
||||
При отказе/таймауте: `status:"fail"`, `detail:"<id> not confirmed"`.
|
||||
|
||||
#### MQS Audio — подтвердить слышимость тона (in-run confirm)
|
||||
|
||||
Таргет ~4 с играет мелодию (A4, затем E5) через MQS + LM4875M, затем запрашивает
|
||||
подтверждение:
|
||||
|
||||
```bash
|
||||
← {"type":"test_begin","id":"mqs",...}
|
||||
← {"type":"confirm_request","id":"mqs_tone","prompt":"Do you hear a tone?","timeout_ms":15000}
|
||||
→ {"type":"confirm","id":"mqs_tone","confirmed":true}
|
||||
← {"type":"test_result","id":"mqs","status":"pass",...}
|
||||
```
|
||||
|
||||
#### Кнопки — нажать физически
|
||||
|
|
@ -472,36 +533,44 @@ main.c
|
|||
|
||||
```bash
|
||||
← {"type":"test_begin","id":"buttons",...}
|
||||
← {"type":"confirm_request","id":"btn1_press","prompt":"Нажмите Test_But_1","timeout_ms":10000}
|
||||
← {"type":"confirm_request","id":"btn1_press","prompt":"Press Test_But_1","timeout_ms":10000}
|
||||
[таргет ждёт bsp_button — без JSON confirm от хоста]
|
||||
← {"type":"confirm_request","id":"btn2_press","prompt":"Нажмите Test_But_2","timeout_ms":10000}
|
||||
← {"type":"test_result","id":"buttons","status":"pass","ms":3821,"detail":""}
|
||||
← {"type":"confirm_request","id":"btn2_press","prompt":"Press Test_But_2","timeout_ms":10000}
|
||||
← {"type":"test_result","id":"buttons","status":"pass",...}
|
||||
```
|
||||
|
||||
Таймаут 10 с → `SKIP` (не FAIL).
|
||||
|
||||
> Полные потоки всех тестов (SDRAM/QSPI/opto/CAN, коды `detail`, HIL pytest) —
|
||||
> в справочнике по тестированию `README_TESTING.md`.
|
||||
|
||||
---
|
||||
|
||||
## Матрица тестов
|
||||
|
||||
| ID | Название | Тип | Critical | M5 HIL | Confirm |
|
||||
| ---------- | ---------------- | ---------------- | -------- | ------ | ------------- |
|
||||
| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
|
||||
| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ |
|
||||
| `usd` | uSD SDIO | self+interactive | ❌ | ❌ | ✅ pre_confirm |
|
||||
| `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ 4×в run() |
|
||||
| `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | prompt only |
|
||||
| `can` | CAN loopback | HIL | ❌ | ✅ | ❌ |
|
||||
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ |
|
||||
| `uart_iso` | UART ISO / RS_RX | HIL | ❌ | ✅ | ❌ |
|
||||
| `opto` | Opto EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ |
|
||||
Порядок — как в реестре `k_registry[]` (`test_runner.c`).
|
||||
|
||||
| № | ID | Название | Тип | Critical | M5 HIL | Confirm |
|
||||
| --- | --------- | ------------------ | ----------- | -------- | ------ | ------------- |
|
||||
| 1 | `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
|
||||
| 2 | `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ |
|
||||
| 3 | `usd` | microSD (SDIO) | interactive | ❌ | ❌ | ✅ pre_confirm |
|
||||
| 4 | `display` | TFT Display RGB888 | interactive | ❌ | ❌ | ✅ 6× в run() |
|
||||
| 5 | `buttons` | Test Buttons | interactive | ❌ | ❌ | prompt only |
|
||||
| 6 | `opto` | Opto Inputs | HIL | ❌ | ✅ | ✅ 6× (авто) |
|
||||
| 7 | `can` | CAN loopback | HIL | ❌ | ✅ | ✅ 2× (авто) |
|
||||
| 8 | `mqs` | MQS Audio Out | interactive | ❌ | ❌ | ✅ 1× в run() |
|
||||
|
||||
**Типы confirm:**
|
||||
|
||||
- **pre_confirm** — test_runner отправляет `confirm_request` до вызова `run()`,
|
||||
ждёт JSON-ответ через state machine (асинхронно).
|
||||
- **pre_confirm** — `test_runner` отправляет `confirm_request` до вызова `run()`,
|
||||
ждёт JSON-ответ через state machine (асинхронно). id = id теста.
|
||||
- **в run()** — тест сам вызывает `test_runner_wait_confirm()` изнутри `run()`,
|
||||
блокируется до ответа (синхронно).
|
||||
- **prompt only** — `protocol_send_confirm_request()` отправляется как UI-подсказка,
|
||||
- **prompt only** — `protocol_send_confirm_request()` как UI-подсказка,
|
||||
хост не отвечает JSON, таргет ждёт физического события.
|
||||
- **авто (HIL)** — confirm генерирует не оператор, а хост-оркестратор, командуя
|
||||
M5StampPLC (см. `README_TESTING.md`).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -540,13 +609,13 @@ static test_result_t test_foo_run(void)
|
|||
return result;
|
||||
}
|
||||
|
||||
const test_module_t k_test_foo = {
|
||||
.id = "foo", /* короткий ASCII-ключ */
|
||||
const test_module_t K_TEST_FOO = {
|
||||
.id = "foo", /* короткий ASCII-ключ */
|
||||
.name = "Foo Peripheral",
|
||||
.critical = false, /* true → run_all стопится при fail */
|
||||
.requires_hil = false, /* true → нужен M5StampPLC */
|
||||
.pre_confirm_prompt = NULL, /* строка → pre_confirm механизм */
|
||||
.init = NULL, /* bsp_foo_init если нужен */
|
||||
.requires_hil = false, /* true → нужен M5StampPLC */
|
||||
.pre_confirm_prompt = NULL, /* строка → pre_confirm механизм */
|
||||
.init = NULL, /* bsp_foo_init если нужен */
|
||||
.run = test_foo_run,
|
||||
.deinit = NULL,
|
||||
};
|
||||
|
|
@ -558,15 +627,19 @@ const test_module_t k_test_foo = {
|
|||
|
||||
```c
|
||||
/* Forward declarations */
|
||||
extern const test_module_t k_test_sdram;
|
||||
extern const test_module_t k_test_foo; /* ← добавить */
|
||||
extern const test_module_t K_TEST_SDRAM;
|
||||
extern const test_module_t K_TEST_FOO; /* ← добавить */
|
||||
|
||||
static const test_module_t *const k_registry[] = {
|
||||
&k_test_sdram,
|
||||
&k_test_foo, /* ← добавить */
|
||||
&K_TEST_SDRAM,
|
||||
...
|
||||
&K_TEST_FOO, /* ← добавить */
|
||||
};
|
||||
```
|
||||
|
||||
> При росте реестра выше `TEST_REGISTRY_MAX_SIZE` (`test_module.h`) сборка
|
||||
> упадёт на `_Static_assert` в `test_runner.c` — увеличить константу.
|
||||
|
||||
### Шаг 3 — Добавить в CMakeLists.txt
|
||||
|
||||
**Файл:** `firmware/test/CMakeLists.txt`
|
||||
|
|
@ -578,7 +651,6 @@ add_executable(
|
|||
src/cli.c
|
||||
src/protocol.c
|
||||
src/test_runner.c
|
||||
src/tests/test_sdram.c
|
||||
src/tests/test_foo.c # ← добавить
|
||||
...
|
||||
)
|
||||
|
|
@ -592,24 +664,18 @@ target_link_libraries(
|
|||
|
||||
### Шаг 4 — Обновить матрицу тестов
|
||||
|
||||
Добавить строку в таблицу в этом README.
|
||||
Добавить строку в таблицу в этом README (и, если есть протокольный поток —
|
||||
в `README_TESTING.md`).
|
||||
|
||||
### Шаблоны для разных типов тестов
|
||||
|
||||
#### Self-тест с инициализацией
|
||||
|
||||
```c
|
||||
static void test_foo_init(void)
|
||||
{
|
||||
bsp_foo_init();
|
||||
}
|
||||
static void test_foo_init(void) { bsp_foo_init(); }
|
||||
static void test_foo_deinit(void) { bsp_foo_deinit(); }
|
||||
|
||||
static void test_foo_deinit(void)
|
||||
{
|
||||
bsp_foo_deinit();
|
||||
}
|
||||
|
||||
const test_module_t k_test_foo = {
|
||||
const test_module_t K_TEST_FOO = {
|
||||
.id = "foo",
|
||||
.init = test_foo_init,
|
||||
.run = test_foo_run,
|
||||
|
|
@ -622,7 +688,6 @@ const test_module_t k_test_foo = {
|
|||
|
||||
```c
|
||||
#include "test_runner.h" /* test_runner_wait_confirm() */
|
||||
#include "protocol.h" /* protocol_send_confirm_request() */
|
||||
|
||||
static test_result_t test_foo_run(void)
|
||||
{
|
||||
|
|
@ -650,13 +715,13 @@ static test_result_t test_foo_run(void)
|
|||
#### Тест с pre_confirm (вставить карту, подключить кабель)
|
||||
|
||||
```c
|
||||
const test_module_t k_test_foo = {
|
||||
const test_module_t K_TEST_FOO = {
|
||||
.id = "foo",
|
||||
.pre_confirm_prompt = "Подключите кабель к разъёму X и нажмите OK",
|
||||
.run = test_foo_run,
|
||||
...
|
||||
};
|
||||
/* test_runner сам отправит confirm_request перед вызовом run() */
|
||||
/* test_runner сам отправит confirm_request (id = "foo") перед вызовом run() */
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -727,34 +792,34 @@ void test_run_all_critical_fail_skips_remaining(void)
|
|||
| `cli_process()` | `FAKE_VOID_FUNC(cli_process)` |
|
||||
| `protocol_send_test_result()` | custom_fake — копируем `*p_result` по значению |
|
||||
|
||||
> **Ловушка dangling pointer:** `protocol_send_test_result` получает указатель
|
||||
> на стековую переменную внутри `execute_test()`. После возврата указатель
|
||||
> инвалиден — используй `custom_fake` с `s_captured = *p_result` пока стек жив.
|
||||
|
||||
---
|
||||
|
||||
## Версионирование
|
||||
|
||||
`FIRMWARE_TEST_VERSION` в `protocol.h` — единственная точка правды о версии.
|
||||
Поле `"fw"` в `session_start` несёт эту строку.
|
||||
Текущая версия ПО задается с помощью `project(firmware_test VERSION X.Y.Z)`
|
||||
в `firmware/test/CMakeLists.txt`. CMake прокидывает её через
|
||||
`configure_file(src/version.h.in → generated/version.h)`, откуда `protocol.h`
|
||||
берёт `FIRMWARE_TEST_VERSION_STR`:
|
||||
|
||||
При несовместимых изменениях протокола (новое обязательное поле, изменение
|
||||
семантики) — bumping версии + обновление этого документа.
|
||||
```
|
||||
CMakeLists.txt: project(firmware_test VERSION 0.1.2)
|
||||
│ configure_file(@ONLY)
|
||||
▼
|
||||
generated/version.h: FIRMWARE_TEST_VERSION_STR = "0.1.2"
|
||||
│
|
||||
▼
|
||||
protocol.h: #define FIRMWARE_TEST_VERSION FIRMWARE_TEST_VERSION_STR
|
||||
│
|
||||
▼
|
||||
session_start / version_response: "fw":"0.1.2"
|
||||
```
|
||||
|
||||
Хост должен сверять `"fw"` при подключении и предупреждать оператора при
|
||||
несовпадении ожидаемой версии.
|
||||
`version.h` генерируется, **не** редактируется вручную. Менять версию —
|
||||
только в `CMakeLists.txt`.
|
||||
|
||||
---
|
||||
Хост может запросить версию явно (`get_version` → `version_response`) или
|
||||
прочитать её из `session_start`, и предупредить оператора при несовпадении
|
||||
с ожидаемой. При несовместимых изменениях протокола (новое обязательное поле,
|
||||
смена семантики) — bump версии + обновление этого документа и `README_TESTING.md`.
|
||||
|
||||
## Архитектурные решения (закрыты)
|
||||
|
||||
> Не пересматривать без явного запроса.
|
||||
|
||||
| Решение | Обоснование |
|
||||
| -------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| Единственный транспорт — USB CDC ACM | HIL ELF-прошивки используют отдельный канал (UART + bsp_uart_host) |
|
||||
| Парсинг JSON — strstr без cJSON | Схема фиксирована, cJSON избыточен |
|
||||
| SDRAM тест — через firmware_test, не HIL ELF | Тест идёт командами по USB CDC |
|
||||
| IR и RTC — не реализуются | Вне scope рекламационной диагностики |
|
||||
| Тесты атомарны | Инженер сам решает что проверять |
|
||||
| Тест-логика на таргете | Хост — тонкий клиент, нет дублирования логики |
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue