Compare commits

..

1 commit

Author SHA1 Message Date
e31de7e3a8 # Fixing filenames 2026-07-02 14:12:00 +03:00
216 changed files with 5748 additions and 18313 deletions

View file

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

View file

@ -15,8 +15,7 @@ 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(), здесь можно переопределить если нужно
@ -62,4 +61,4 @@ HIL_USB_CDC_BAUD=115200
HIL_USB_CDC_TIMEOUT=5.0
# Тип сборки firmware_test для TUI (Debug | Release)
FIRMWARE_BUILD_TYPE=Debug
FIRMWARE_BUILD_TYPE=Release

View file

@ -1,338 +0,0 @@
name: Release
# Раздельные теги (bootloader — Фаза 5, firmware/bootloader/PLAN.md):
# firmware_test, bootloader и service-tui версионируются и релизятся
# независимо друг от друга.
# firmware-vX.Y.Z → publish-firmware (только firmware_test_hab.bin, Debug)
# bootloader-vX.Y.Z → publish-bootloader (только bootloader_hab.bin, Release,
# подписан ТЕСТОВЫМ HAB-ключом — см.
# firmware/bootloader/SIGNING_CEREMONY.md)
# tui-vX.Y.Z → publish-tui (service-tui-vX.Y.Z-{macos,windows}.zip)
# HAB firmware_test/bootloader, вшиваемые в TUI-бандл, всегда собираются заново
# из текущего HEAD тега tui-v*, а не берутся из отдельных релизов — так проще
# и не тянет зависимость на чужой GitHub Release.
on:
push:
tags:
- "tui-v*"
- "firmware-v*"
- "bootloader-v*"
workflow_dispatch:
inputs:
release_type:
description: "Тестовый прогон без публикации (job publish-* не запускается вне push тега)"
type: choice
options:
- tui
- firmware
- bootloader
default: tui
concurrency:
group: release-${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
jobs:
# ─────────────────────────────────────────────────────────────────────────
# firmware — собирает HAB Debug firmware_test И HAB Release bootloader.
# Нужна как для standalone firmware-/bootloader-релизов, так и для вшивания
# обоих образов в TUI-бандл (package-tui, Фаза 5) — выполняется всегда.
# ─────────────────────────────────────────────────────────────────────────
firmware:
name: Build firmware_test HAB (Debug) + bootloader HAB (Release)
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build devcontainer image with cache
uses: docker/build-push-action@v7
with:
context: .
file: .devcontainer/Dockerfile
tags: tft-devcontainer-ci:latest
load: true
cache-from: type=gha,scope=tft-devcontainer
cache-to: type=gha,mode=max,scope=tft-devcontainer
- name: Sync host Python tools inside container
run: |
docker run --rm --user root -v "$GITHUB_WORKSPACE":/workspace -w /workspace \
tft-devcontainer-ci:latest bash -lc 'cd tools/host && uv sync'
- name: Проверить, что тег совпадает с версией в CMakeLists.txt
if: startsWith(github.ref, 'refs/tags/firmware-v')
run: |
tag_version="${GITHUB_REF_NAME#firmware-v}"
file_version=$(grep -m1 -oE 'VERSION [0-9]+\.[0-9]+\.[0-9]+' firmware/test/CMakeLists.txt | awk '{print $2}')
if [[ "$tag_version" != "$file_version" ]]; then
echo "::error::Тег ${GITHUB_REF_NAME} (версия ${tag_version}) не совпадает с VERSION в firmware/test/CMakeLists.txt (${file_version})"
exit 1
fi
echo " ✅ Версия тега совпадает с CMakeLists.txt: ${file_version}"
- name: Собрать firmware_test HAB (Debug)
run: |
docker run --rm --user root -v "$GITHUB_WORKSPACE":/workspace -w /workspace \
tft-devcontainer-ci:latest bash -lc 'just build::hab-firmware-test-debug'
- name: Upload firmware_test_hab.bin
uses: actions/upload-artifact@v4
with:
name: firmware-hab-debug
path: build/Debug/firmware_test_hab.bin
if-no-files-found: error
retention-days: 14
- name: Проверить, что тег совпадает с версией в CMakeLists.txt (bootloader)
if: startsWith(github.ref, 'refs/tags/bootloader-v')
run: |
tag_version="${GITHUB_REF_NAME#bootloader-v}"
file_version=$(grep -m1 -oE 'VERSION [0-9]+\.[0-9]+\.[0-9]+' firmware/bootloader/CMakeLists.txt | awk '{print $2}')
if [[ "$tag_version" != "$file_version" ]]; then
echo "::error::Тег ${GITHUB_REF_NAME} (версия ${tag_version}) не совпадает с VERSION в firmware/bootloader/CMakeLists.txt (${file_version})"
exit 1
fi
echo " ✅ Версия тега совпадает с CMakeLists.txt: ${file_version}"
- name: Собрать bootloader HAB (Release)
run: |
docker run --rm --user root -v "$GITHUB_WORKSPACE":/workspace -w /workspace \
tft-devcontainer-ci:latest bash -lc 'just build::hab-bootloader-release'
- name: Upload bootloader_hab.bin
uses: actions/upload-artifact@v4
with:
name: bootloader-hab-release
path: build/Release/bootloader_hab.bin
if-no-files-found: error
retention-days: 14
# ─────────────────────────────────────────────────────────────────────────
# publish-firmware — только на тег firmware-v*, отдельный standalone-релиз
# HAB-образа (для tools/host/flash_usb.py и ручной прошивки, не через TUI).
# ─────────────────────────────────────────────────────────────────────────
publish-firmware:
name: Publish firmware release
needs: firmware
if: startsWith(github.ref, 'refs/tags/firmware-v')
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download firmware_test_hab.bin
uses: actions/download-artifact@v4
with:
name: firmware-hab-debug
path: release-assets/
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${GITHUB_REF_NAME}" \
release-assets/firmware_test_hab.bin \
--title "firmware_test ${GITHUB_REF_NAME#firmware-v}" \
--generate-notes
# ─────────────────────────────────────────────────────────────────────────
# publish-bootloader — только на тег bootloader-v*, отдельный standalone-
# релиз HAB-образа (для tools/host/flash_usb.py / SWD, не через TUI).
# ⚠️ Подписан ТЕСТОВЫМ HAB-ключом (dev/pre-series, HAB Open чип всё равно
# загрузит и unsigned) — реальная SRK-церемония не проведена, см.
# firmware/bootloader/SIGNING_CEREMONY.md. Явно проговорено в release notes,
# чтобы этот asset не приняли за production-подписанный постфактум.
# ─────────────────────────────────────────────────────────────────────────
publish-bootloader:
name: Publish bootloader release
needs: firmware
if: startsWith(github.ref, 'refs/tags/bootloader-v')
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download bootloader_hab.bin
uses: actions/download-artifact@v4
with:
name: bootloader-hab-release
path: release-assets/
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${GITHUB_REF_NAME}" \
release-assets/bootloader_hab.bin \
--title "bootloader ${GITHUB_REF_NAME#bootloader-v}" \
--notes "⚠️ HAB-подпись — ТЕСТОВЫЙ ключ (dev/pre-series, HAB Open). Не production. Реальная SRK-церемония — firmware/bootloader/SIGNING_CEREMONY.md."
# ─────────────────────────────────────────────────────────────────────────
# service-tui-{macos,windows} — упаковка PyInstaller-бандла с вшитым HAB
# из job firmware. Запускается на тег tui-v* и на workflow_dispatch с
# release_type=tui (для тестового прогона без публикации).
# ─────────────────────────────────────────────────────────────────────────
service-tui-macos:
name: Package service-tui (macOS)
needs: firmware
if: startsWith(github.ref, 'refs/tags/tui-v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_type == 'tui')
runs-on: macos-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v8.3.2 # без плавающего major-тега начиная с v8 (upstream security policy) — пин точной версии, обновлять периодически
- name: Install just
uses: extractions/setup-just@v4
- name: Download firmware_test_hab.bin
uses: actions/download-artifact@v4
with:
name: firmware-hab-debug
path: build/Debug/
- name: Download bootloader_hab.bin
uses: actions/download-artifact@v4
with:
name: bootloader-hab-release
path: build/Release/
- name: Проверить, что тег совпадает с версией в pyproject.toml
if: startsWith(github.ref, 'refs/tags/tui-v')
run: |
tag_version="${GITHUB_REF_NAME#tui-v}"
file_version=$(grep -m1 '^version' tools/service_tui/pyproject.toml | sed -E 's/.*"(.+)".*/\1/')
if [[ "$tag_version" != "$file_version" ]]; then
echo "::error::Тег ${GITHUB_REF_NAME} (версия ${tag_version}) не совпадает с version в tools/service_tui/pyproject.toml (${file_version})"
exit 1
fi
echo " ✅ Версия тега совпадает с pyproject.toml: ${file_version}"
- name: Собрать зависимости и упаковать бандл
run: |
just host::service-setup
just host::package-tui
- name: Заархивировать бандл
working-directory: tools/service_tui/dist
run: |
name=$(ls -d service-tui-v*-macos)
zip -r "${name}.zip" "${name}"
- name: Upload bundle
uses: actions/upload-artifact@v4
with:
name: service-tui-macos
path: tools/service_tui/dist/service-tui-v*-macos.zip
if-no-files-found: error
retention-days: 14
service-tui-windows:
name: Package service-tui (Windows)
needs: firmware
if: startsWith(github.ref, 'refs/tags/tui-v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_type == 'tui')
runs-on: windows-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v8.3.2 # без плавающего major-тега начиная с v8 (upstream security policy) — пин точной версии, обновлять периодически
- name: Install just
uses: extractions/setup-just@v4
- name: Download firmware_test_hab.bin
uses: actions/download-artifact@v4
with:
name: firmware-hab-debug
path: build/Debug/
- name: Download bootloader_hab.bin
uses: actions/download-artifact@v4
with:
name: bootloader-hab-release
path: build/Release/
- name: Проверить, что тег совпадает с версией в pyproject.toml
if: startsWith(github.ref, 'refs/tags/tui-v')
shell: bash
run: |
tag_version="${GITHUB_REF_NAME#tui-v}"
file_version=$(grep -m1 '^version' tools/service_tui/pyproject.toml | sed -E 's/.*"(.+)".*/\1/')
if [[ "$tag_version" != "$file_version" ]]; then
echo "::error::Тег ${GITHUB_REF_NAME} (версия ${tag_version}) не совпадает с version в tools/service_tui/pyproject.toml (${file_version})"
exit 1
fi
echo " ✅ Версия тега совпадает с pyproject.toml: ${file_version}"
- name: Собрать зависимости и упаковать бандл
shell: bash
run: |
just host::service-setup
just host::package-tui
- name: Заархивировать бандл
working-directory: tools/service_tui/dist
shell: pwsh
run: |
$name = (Get-ChildItem -Directory -Filter "service-tui-v*-windows").Name
Compress-Archive -Path $name -DestinationPath "$name.zip"
- name: Upload bundle
uses: actions/upload-artifact@v4
with:
name: service-tui-windows
path: tools/service_tui/dist/service-tui-v*-windows.zip
if-no-files-found: error
retention-days: 14
# ─────────────────────────────────────────────────────────────────────────
# publish-tui — только на тег tui-v*, релиз с обоими zip-бандлами.
# ─────────────────────────────────────────────────────────────────────────
publish-tui:
name: Publish service-tui release
needs: [service-tui-macos, service-tui-windows]
if: startsWith(github.ref, 'refs/tags/tui-v')
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download bundles
uses: actions/download-artifact@v4
with:
pattern: service-tui-*
path: release-assets/
merge-multiple: true
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${GITHUB_REF_NAME}" \
release-assets/*.zip \
--title "service-tui ${GITHUB_REF_NAME#tui-v}" \
--generate-notes

1
.gitignore vendored
View file

@ -77,4 +77,3 @@ tools/host/.venv-host/
tools/host/.venv-host-win/
.zed/
project_tree.txt
tools/service_tui/dist/

2
.vscode/launch.json vendored
View file

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

View file

@ -1,5 +1,5 @@
{
"[python]": {
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff"
}

View file

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

View file

@ -46,10 +46,8 @@ add_subdirectory(lib)
# -----------------------------------------------------------------------------
if(NOT BUILD_TESTS_HOST)
add_subdirectory(firmware/test)
add_subdirectory(firmware/bootloader)
# Заглушка tft_app для аппаратной верификации bootutil (Фаза 2) — удалить,
# когда появится реальный firmware/tft_app. См. firmware/bootloader/PLAN.md.
add_subdirectory(firmware/bootloader/test_stub)
# add_subdirectory(firmware/bootloader) - add_subdirectory(firmware/app) #
# Загрузчик + Основное приложение
endif()
# -----------------------------------------------------------------------------

View file

@ -126,19 +126,6 @@
"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",
@ -163,11 +150,7 @@
"test_prio_queue",
"uart_host_mock_example",
"test_ring_buffer",
"test_timeout_pattern",
"test_mcuboot_boot_select",
"test_slot_version",
"test_update_policy",
"test_recovery"
"test_timeout_pattern"
]
},
{
@ -186,11 +169,7 @@
"test_prio_queue",
"uart_host_mock_example",
"test_ring_buffer",
"test_timeout_pattern",
"test_mcuboot_boot_select",
"test_slot_version",
"test_update_policy",
"test_recovery"
"test_timeout_pattern"
]
},
{

View file

@ -10,20 +10,10 @@
## Firmware-проекты
| Проект | Путь | Описание |
| ----------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Тестовая прошивка | `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) |
| ------------------- | ---------------------- | -------------------------------------------------------------------------------------- |
| Тестовая прошивка | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS |
| Загрузчик | `firmware/bootloader/` | A/B обновление через uSD. Обновляется только через USB ROM + blhost / SWD |
| Production прошивка | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком |
---
@ -68,6 +58,20 @@ 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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_callback_t cb, void *p_ctx);
bsp_status_t bsp_can_register_rx_callback(bsp_can_rx_cb_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/can.c
${PROJECT_SOURCE_DIR}/bsp/can/src/bsp_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_mocks.h"
#include "can_mock.h"
void setUp(void) { CAN_MOCK_RESET_ALL(); }

View file

@ -161,15 +161,18 @@ 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))
@ -177,6 +180,7 @@ 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++;
}
@ -218,13 +222,16 @@ 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);
@ -299,6 +306,7 @@ 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;
@ -414,12 +422,14 @@ 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();
@ -432,6 +442,7 @@ 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;
@ -460,13 +471,16 @@ 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)
{

View file

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

View file

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

View file

@ -28,27 +28,9 @@ static sd_io_voltage_t s_io_voltage = {
volatile uint32_t g_sdmmc_dbg_dma_buf_addr = 0U;
volatile uint32_t g_sdmmc_dbg_usdhc1_src_clock_hz = 0U;
/*
* Детект карты через 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)
static bool sd_card_detect_gpio(void)
{
CLOCK_EnableClock(kCLOCK_Usdhc1);
return (USDHC_GetPresentStatusFlags(BOARD_SDMMC_SD_HOST_BASEADDR) &
(uint32_t) kUSDHC_CardInsertedFlag) != 0U;
return GPIO_PinRead(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN) == BOARD_SDMMC_SD_CD_INSERT_LEVEL;
}
/* ---------------------------------------------------------------------------
@ -67,8 +49,7 @@ static uint32_t get_usdhc1_src_clock_hz(void)
}
/*
* Управление питанием карты: GPIO1[19] (SdPwr). Регистрируется как
* usrParam.pwr SDK дёргает её из SD_SetCardPower().
* Управление питанием карты: GPIO1[19] (SdPwr), active-high.
*/
static void sd_power_control(bool enable)
{
@ -142,12 +123,10 @@ static void sd_pin_config(uint32_t freq)
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_03_USDHC1_DATA1, pad);
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_04_USDHC1_DATA2, pad);
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_05_USDHC1_DATA3, pad);
/*
* CD (D13) здесь НЕ конфигурируем: пин на USDHC1_CD_B (см. BOARD_SD_Config),
* pad задан в BOARD_InitPins() и на железе даёт корректный CINST. Прежняя
* настройка pad'а GPIO2_IO28 убрана вместе с GPIO-детектом (item 1,
* DEBUG_LOG_PHASE3_SD.md).
*/
/* 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));
}
/* ---------------------------------------------------------------------------
@ -172,10 +151,10 @@ void BOARD_SD_Config(void *card, sd_cd_t cd, uint32_t host_irq_priority, void *u
g_sdmmc_dbg_dma_buf_addr = (uint32_t)(uintptr_t)s_dma_buf;
g_sdmmc_dbg_usdhc1_src_clock_hz = sd->host->hostController.sourceClock_Hz;
/* --- card detect: USDHC PRES_STATE.CINST через callback (polling, без IRQ) --- */
/* --- card detect: GPIO CD (active-low) --- */
s_cd.cdDebounce_ms = BOARD_SDMMC_SD_CD_DEBOUNCE_MS;
s_cd.type = BOARD_SDMMC_SD_CD_TYPE; /* kSD_DetectCardByGpioCD → callback ниже */
s_cd.cardDetected = sd_card_detect_prsstat;
s_cd.type = BOARD_SDMMC_SD_CD_TYPE;
s_cd.cardDetected = sd_card_detect_gpio;
s_cd.callback = cd; /* обычно NULL из bsp_sd */
s_cd.userData = user_data;
@ -187,20 +166,15 @@ void BOARD_SD_Config(void *card, sd_cd_t cd, uint32_t host_irq_priority, void *u
/* --- GPIO питания --- */
sd_power_init();
/*
* 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). */
/* 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). */
sd_pin_config(400000U);
/* --- приоритет прерывания хоста --- */

View file

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

View file

@ -1,10 +1,10 @@
/**
* @file bsp/mqs.h
* @brief BSP: Medium Quality Sound (MQS) SAI3 + eDMA + MQS.
* @brief BSP: Medium Quality Sound (MQS) SAI1 + eDMA + MQS.
*
* Слой абстракции над SAI3/eDMA/MQS для монофонического аудио-выхода.
* Слой абстракции над SAI1/eDMA/MQS для монофонического аудио-выхода.
* Физически на плате выведен один канал (MQS_RIGHT, GPIO_AD_B0_04);
* SAI3 требует стерео-буфер оба канала всегда идентичны.
* SAI1 требует стерео-буфер оба канала всегда идентичны.
*
* Режимы использования:
* - firmware_test: bsp_mqs_play_blocking() синхронная подача
@ -38,14 +38,14 @@ extern "C"
* Параметры аудио-потока
* ----------------------------------------------------------------------- */
/** Частота дискретизации, Гц. Источник — SAI3_CLK_ROOT (Audio PLL / 8 / 8),
* делитель MCLK подобран точно (8), отклонения нет. */
/** Частота дискретизации, Гц. Небольшое отклонение (~0.5%) из-за
* источника SAI1_CLK_ROOT (System PLL PFD2, не Audio PLL). */
#define BSP_MQS_SAMPLE_RATE_HZ (44100U)
/** Разрядность PCM. MQS поддерживает только 16 бит. */
#define BSP_MQS_BIT_WIDTH (16U)
/** Количество каналов в буфере. SAI3+MQS требует стерео; правый == левый. */
/** Количество каналов в буфере. SAI1+MQS требует стерео; правый == левый. */
#define BSP_MQS_CHANNELS (2U)
/** Байт на один моно-сэмпл (16 бит → 2 байта). */
@ -73,11 +73,11 @@ extern "C"
* ----------------------------------------------------------------------- */
/**
* @brief Инициализация MQS-подсистемы: SAI3, eDMA, DMAMUX, MQS.
* @brief Инициализация MQS-подсистемы: SAI1, eDMA, DMAMUX, MQS.
*
* Включает тактирование SAI3 (kCLOCK_Sai3), настраивает SAI3 в режиме
* Включает тактирование SAI1 (kCLOCK_Sai1), настраивает SAI1 в режиме
* TX Master, 16 бит, стерео, 44100 Гц, инициализирует eDMA канал 0
* (DMAMUX source kDmaRequestMuxSai3Tx) и MQS-модуль.
* (DMAMUX source kDmaRequestMuxSai1Tx) и 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, сбросить SAI3 и MQS.
* @brief Деинициализация: остановить DMA, сбросить SAI1 и MQS.
*
* Безопасно вызывать даже если воспроизведение уже завершилось.
* После вызова модуль требует повторного bsp_mqs_init().
@ -152,7 +152,7 @@ extern "C"
* ----------------------------------------------------------------------- */
/**
* @brief Инициализация усилителя: PWM4 SM0, 12 кГц, duty 50%.
* @brief Инициализация усилителя: PWM4 SM0, 16 кГц, duty 50%.
*
* Настраивает XBARA1 (fault disable), PWM4 submodule 0 channel A.
* Вызывать до bsp_mqs_play() без ШИМ на VOLUME усиление равно нулю.

View file

@ -1,21 +1,19 @@
/**
* @file bsp_mqs.c
* @brief BSP MQS: SAI3 TX + eDMA + MQS для MIMXRT1052CVJ5B.
* @brief BSP MQS: SAI1 TX + eDMA + MQS для MIMXRT1052CVJ5B.
*
* Тактирование:
* 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)
* SAI1_CLK_ROOT = SysPLL × (18/27) / (SAI1_CLK_PRED+1=4) / (SAI1_CLK_PODF+1=2)
* 63 529 411 Гц (BOARD_BOOTCLOCKRUN_SAI1_CLK_ROOT)
* Bit clock = 44100 × 16 × 2 = 1 411 200 Гц
* MCLK делитель = 11 289 600 / 1 411 200 = 8 (точно, без погрешности)
* MCLK делитель = 63 529 411 / 1 411 200 45.0 (погрешность ~0.5 %)
*
* 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 kDmaRequestMuxSai3Tx.
* eDMA: DMA0 канал 0, DMAMUX source kDmaRequestMuxSai1Tx.
* Канал 0 зарезервирован за bsp_mqs. Прочие модули каналы 1+.
*
* SAI API (SDK 2.4.7 / fsl_sai.h, fsl_sai_edma.h 2.7.3):
@ -49,17 +47,17 @@
#define MQS_SAI_CLOCK_GATE kCLOCK_Sai3
#define MQS_SAI_CLK_FREQ BOARD_BOOTCLOCKRUN_SAI3_CLK_ROOT
/** eDMA канал, выделенный под SAI3 TX. */
/** eDMA канал, выделенный под SAI1 TX. */
#define MQS_DMA_CHANNEL (0U)
/** DMAMUX запрос для SAI3 TX. */
/** DMAMUX запрос для SAI1 TX. */
#define MQS_DMAMUX_SOURCE kDmaRequestMuxSai3Tx
/** Приоритет прерывания DMA (ниже USB = 3, выше нормальных задач). */
#define MQS_DMA_IRQ_PRIORITY (5U)
#define MQS_HMCLK_GATE kCLOCK_Mqs
/**
* FIFO watermark половина глубины FIFO SAI3.
* FIFO watermark половина глубины FIFO SAI1.
* FSL_FEATURE_SAI_FIFO_COUNTn(x) принимает экземпляр SAI и возвращает
* глубину FIFO в словах (32 для RT1052). Деление на 2 даёт оптимальную
* латентность DMA: запрос формируется когда в FIFO остаётся место для
@ -110,6 +108,11 @@ 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)
{
@ -118,7 +121,7 @@ bsp_status_t bsp_mqs_init(void)
return BSP_OK;
}
/* --- Тактирование SAI3 --- */
/* --- Тактирование SAI1 --- */
CLOCK_EnableClock(MQS_SAI_CLOCK_GATE);
/* --- Тактирование MQS (CCGR0[CG2]) --- */
@ -129,10 +132,10 @@ bsp_status_t bsp_mqs_init(void)
IOMUXC_MQSEnterSoftwareReset(IOMUXC_GPR, false);
IOMUXC_MQSEnable(IOMUXC_GPR, true);
/* --- SAI3: базовая инициализация (снимает reset, включает clock gate) --- */
/* --- SAI1: базовая инициализация (снимает reset, включает clock gate) --- */
SAI_Init(MQS_SAI_BASE);
/* --- SAI3 TX: классический I2S, master, 16 бит, стерео, канал 0. --- */
/* --- SAI1 TX: классический I2S, master, 16 бит, стерео, канал 0. --- */
sai_transceiver_t sai_cfg;
SAI_GetLeftJustifiedConfig(&sai_cfg, kSAI_WordWidth16bits, kSAI_Stereo,
@ -156,7 +159,7 @@ bsp_status_t bsp_mqs_init(void)
EDMA_Init(DMA0, &dma_cfg);
EDMA_CreateHandle(&s_dma_handle, DMA0, MQS_DMA_CHANNEL);
/* --- DMAMUX: канал 0 → SAI3 TX --- */
/* --- DMAMUX: канал 0 → SAI1 TX --- */
DMAMUX_Init(DMAMUX);
DMAMUX_SetSource(DMAMUX, MQS_DMA_CHANNEL, (uint8_t) MQS_DMAMUX_SOURCE);
DMAMUX_EnableChannel(DMAMUX, MQS_DMA_CHANNEL);
@ -185,7 +188,7 @@ void bsp_mqs_deinit(void)
}
SAI_TransferTerminateSendEDMA(MQS_SAI_BASE, &s_sai_tx_handle);
/* SAI_TxReset() — сброс logic + FIFO (аналог kSAI_ResetAll) для SAI3.
/* SAI_TxReset() — сброс logic + FIFO (аналог kSAI_ResetAll).
* SAI_TxSoftwareReset() отсутствует в данной версии SDK. */
SAI_TxReset(MQS_SAI_BASE);
IOMUXC_MQSEnable(IOMUXC_GPR, false);

View file

@ -9,7 +9,7 @@
* аудио-сигнала на входе (MQS_RIGHT через RC-фильтр SOUND_OUT).
*
* Управление громкостью:
* PWM4 SM0 PWM_A, частота 12 кГц, центрально-симметричный режим.
* PWM4 SM0 PWM_A, частота 16 кГц, центрально-симметричный режим.
* 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 = 9 375 000 / 586 / 2 = 12000 Гц (центрально-симметричный режим).
* Fpwm = 16000 Гц (центрально-симметричный режим).
*/
#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-сигнал: 12 кГц, центрально-симметричный, duty 50% --- */
/* --- PWM-сигнал: 16 кГц, центрально-симметричный, duty 50% --- */
const pwm_signal_param_t PWM_SIGNAL = {
.pwmChannel = AMP_PWM_CHANNEL,
.dutyCyclePercent = AMP_DEFAULT_DUTY,

View file

@ -12,6 +12,7 @@
* Пин LOW (тока нет) BSP_OPTO_STATE_INACTIVE
*
* Режимы каналов (bsp_opto_ch_mode_t):
* ы
* BSP_OPTO_MODE_LEVEL IN1, IN2
* Детектирование уровня с программным дебаунсом.
* ISR переключает направление прерывания (RISINGFALLING) после каждого фронта,

View file

@ -76,6 +76,7 @@ target_link_libraries(firmware_test PRIVATE bsp_provisioning)
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_board` | PRIVATE | Транзитивно: clock, SDK headers |
---
## Особенности

View file

@ -25,8 +25,9 @@
* @brief Прочитать уникальный идентификатор чипа из OCOTP.
*
* Читает OCOTP_CFG0 (UID[31:0]) и OCOTP_CFG1 (UID[63:32]).
* Результат записывается в нативном порядке байт (little-endian на Cortex-M7):
* p_uid[0..3] = CFG0 (UID[31:0]), p_uid[4..7] = CFG1 (UID[63:32]).
* Результат записывается в big-endian порядке: p_uid[0] старший байт CFG1,
* p_uid[7] младший байт CFG0. Hex-строка совпадает с визуальным порядком слов
* в Reference Manual (MIMXRT1052RM Table 46-2).
*
* Функция выполняет OCOTP_Init() и включает clock gate перед чтением.
* Clock gate остаётся открытым после вызова (паттерн проекта).

View file

@ -77,9 +77,8 @@ 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() */
/* Стирание */
@ -118,13 +117,9 @@ board_hw_init();
bsp_qspi_init(); /* ← до bsp_tick_init() */
bsp_tick_init();
/* Идентификация чипа — работает даже если bsp_qspi_init() выше вернула
ошибку (LUT для JEDEC грузится безусловным первым шагом внутри неё) */
/* Идентификация чипа */
bsp_qspi_jedec_t jedec;
bsp_qspi_read_jedec_id(&jedec);
uint32_t size_mb;
const char *chip_name = bsp_qspi_decode_chip((uint8_t) (jedec.device_id & 0xFF), &size_mb);
/* chip_name = "W25Q128", size_mb = 16 — либо "UNKNOWN"/0, если чип не опознан */
/* Стереть сектор и записать страницу */
bsp_qspi_erase_sector(0x00010000);

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,9 @@
# bsp_sdram — внешняя SDRAM MT48LC16M16A2 (32 МБ)
Подъём SEMC (для прошивок без DCD) и минимальная верификация доступности
внешней SDRAM, подключённой к SEMC. Подробное тестирование (паттерны, шина
адреса/данных, retention) выполняется в тест-модуле `firmware_test/test_sdram.c`,
который использует API этого модуля (путь с DCD, см. ниже).
Минимальная верификация доступности внешней SDRAM, подключённой к SEMC.
Подробное тестирование (паттерны, шина адреса/данных, retention) выполняется
в тест-модуле `firmware_test/test_sdram.c`, который использует константы
и API этого модуля.
---
@ -29,45 +29,16 @@
Тестовая база смещена на 2 МБ от начала — гарантированно выше `.data`/`.bss`
прошивки и ниже non-cacheable региона.
---
## Контракт: кто поднимает 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()` дублировала бы
её же на живой памяти.
**Важно:** SEMC инициализируется через DCD **до вызова `main()`**. Этот модуль
не настраивает SEMC и не трогает его регистры. Если DCD не отработал —
`bsp_sdram_init()` вернёт ошибку, но исправить ситуацию из модуля нельзя.
---
## API
```c
bsp_status_t bsp_sdram_configure(void); /* поднять SEMC: тактирование → пины → контроллер → init-команды SDRAM */
bsp_status_t bsp_sdram_init(void); /* верифицировать SDRAM (SEMC уже поднят — DCD либо bsp_sdram_configure()) */
bsp_status_t bsp_sdram_init(void);
```
**Публичные константы:**
@ -84,37 +55,6 @@ bsp_status_t bsp_sdram_init(void); /* верифицировать SDRAM (S
Константы размеров — для потребителей; `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`),
@ -126,13 +66,11 @@ ROM пишет регистры до включения MPU. Уже добавл
**Коды возврата:**
| Функция | Код | Условие |
| -------------------------- | ----------------- | -------------------------------------------- |
| `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 |
| Код | Условие |
| ----------------- | ------------------------------------------- |
| `BSP_OK` | SDRAM доступна, оба паттерна совпали |
| `BSP_ERR_TIMEOUT` | SEMC не перешёл в IDLE за 10 мс |
| `BSP_ERR_INIT` | Хотя бы один паттерн не совпал при readback |
---
@ -141,19 +79,10 @@ ROM пишет регистры до включения MPU. Уже добавл
```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) */
```
@ -162,10 +91,6 @@ if (bsp_sdram_init() != BSP_OK) {
## CMake
Сегодня линкует только `firmware_test` (путь с DCD). Для пути без DCD
потребитель (например, bootloader) добавляет зависимость сам — модуль
никого за собой не тянет:
```cmake
target_link_libraries(firmware_test PRIVATE bsp_sdram)
```
@ -173,8 +98,8 @@ 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->`, `SEMC_SendIPCommand()` |
| `bsp_board` | PRIVATE | Транзитивно даёт `sdk_clock` (`CLOCK_Init*()`) и `sdk_device` (`IOMUXC`) — отдельных PRIVATE-строк на них не заводили |
| `sdk_semc` | PRIVATE | `fsl_semc.h``SEMC->STS0`, `SEMC_STS0_IDLE_MASK` |
| `bsp_board` | PRIVATE | Общие board-уровневые символы |

View file

@ -2,15 +2,9 @@
* @file sdram.h
* @brief BSP для внешней SDRAM MT48LC16M16A2 (32 МБ, шина 16 бит).
*
* Два пути в зависимости от того, кто использует 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() верифицирует, как и в первом случае.
* Архитектурное ограничение [DECISION]:
* SEMC инициализируется DCD до вызова main(). Этот модуль не трогает
* регистры SEMC только верифицирует работоспособность памяти.
*
* Карта памяти:
* 0x80000000 начало SDRAM (SEMC BR0)
@ -20,10 +14,16 @@
* Тестовый регион (не пересекается с .data/.bss и non-cacheable):
* 0x80200000 начало (2 MB offset от базы)
*
* Использование (путь без DCD):
* Использование:
* @code
* if (bsp_sdram_configure() != BSP_OK) { handle_semc_bringup_error(); }
* if (bsp_sdram_init() != BSP_OK) { handle_sdram_error(); }
* bsp_sdram_result_t result;
*
* if (bsp_sdram_init() != BSP_OK) { // DCD не отработал
* handle_critical_error();
* }
*
* bsp_sdram_test_fast(&result); // ~50 мс, 64 KB
* bsp_sdram_test_full(&result); // ~25 с, 1 MB
* @endcode
*/
@ -65,51 +65,17 @@
/* ── 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 или bsp_sdram_configure())
* и SDRAM отвечает выполняет минимальный write/read/verify на первых 4 байтах
* тестового региона. Не затрагивает .data/.bss прошивки.
* Проверяет что SEMC контроллер инициализирован DCD и SDRAM отвечает
* выполняет минимальный write/read/verify на первых 4 байтах тестового
* региона. Не затрагивает .data/.bss прошивки.
*
* @note Не (ре)инициализирует SEMC предполагает, что DCD либо
* bsp_sdram_configure() уже это сделали.
* @note Не реинициализирует SEMC DCD уже сделал это до main().
*
* @retval BSP_OK SDRAM доступна и отвечает корректно.
* @retval BSP_ERR_INIT SEMC не готов (инициализация не отработала).
* @retval BSP_ERR_INIT SEMC не готов (DCD не отработал).
* @retval BSP_ERR_TIMEOUT SEMC занят дольше ожидаемого.
*/
bsp_status_t bsp_sdram_init(void);

View file

@ -1,14 +1,9 @@
/**
* @file sdram.c
* @brief Подъём SEMC и верификация внешней SDRAM MT48LC16M16A2 (32 МБ, 16 бит).
* @brief Верификация внешней SDRAM MT48LC16M16A2 (32 МБ, шина 16 бит).
*
* Два независимых куска:
*
* bsp_sdram_configure() поднимает SEMC. Побитовый порт проверенной DCD-последовательности
* (tools/host/dcd/dcd.bin)
*
* bsp_sdram_init() верификация уже поднятой памяти (DCD или configure).
* Регистры SEMC не модифицирует.
* SEMC инициализируется DCD до main() этот модуль только проверяет
* доступность памяти. Регистры SEMC не модифицируются.
*
* Кэш: SDRAM настроена как Normal Write-Back cacheable (MPU Region 8).
* Верификация требует явного cache maintenance перед readback иначе
@ -18,13 +13,12 @@
#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
@ -44,202 +38,11 @@
/** @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 g_s_initialised = false;
static bool s_initialised = false;
/**
* @brief Поднять тактовую цепочку SEMC (порт секции «Clock Init» DCD).
*
*/
static void sdram_configure_clock(void)
{
/* DCD PLL_SYS=0x00002001 → Fout = 24 МГц × (20 + 2×loopDivider + num/denom)
* = 24 × (20 + 2 + 0) = 528 МГц. */
static const clock_sys_pll_config_t SYS_PLL = {
.loopDivider = 1, .numerator = 0, .denominator = 1, .src = 0, /* 24 МГц OSC */
};
/* Гейтим SEMC на время переключения его источника (идиома clock_config.c). */
CLOCK_DisableClock(kCLOCK_Semc);
CLOCK_InitSysPll(&SYS_PLL);
CLOCK_InitSysPfd(kCLOCK_Pfd2, SDRAM_SEMC_PFD2_FRAC);
/* DCD CBCDR=0x00010D40 → SEMC_CLK_SEL=1 (alt), SEMC_ALT_CLK_SEL=0 (→PFD2),
* SEMC_PODF=1 (÷2). */
CLOCK_SetMux(kCLOCK_SemcAltMux, 0); /* alt = PLL2 PFD2 */
CLOCK_SetMux(kCLOCK_SemcMux, 1); /* SEMC clock = alt (PFD2), не periph_clk */
CLOCK_SetDiv(kCLOCK_SemcDiv, kCLOCK_SemcDivBy2);
CLOCK_EnableClock(kCLOCK_Semc);
}
/* ── SEMC: пины ─────────────────────────────────────────────────── */
/**
* @brief Замуксить пины GPIO_EMC на функцию SEMC + PAD-настройки (порт DCD).
*
*/
static void sdram_configure_pins(void)
{
for (uint32_t i = SDRAM_EMC_PAD_FIRST; i <= SDRAM_EMC_PAD_LAST; i++)
{
IOMUXC->SW_MUX_CTL_PAD[i] = 0UL; /* ALT0 = функция SEMC */
IOMUXC->SW_PAD_CTL_PAD[i] = SDRAM_PAD_CTL_VALUE;
}
/* DQS: ALT0 + SION (DCD пишет сюда 0x10 вместо 0x00). */
IOMUXC->SW_MUX_CTL_PAD[SDRAM_DQS_PAD_INDEX] = SDRAM_MUX_SION;
}
/* ── SEMC: регистры контроллера ─────────────────────────────────── */
/**
* @brief Записать регистры контроллера SEMC (дословно «блок 2» DCD).
*
*/
static void sdram_configure_controller(void)
{
SEMC->MCR = 0x10000004UL; /* модуль вкл (MDIS=0), DQSMD=1 (DQS с пина), BTO=16 */
SEMC->BMCR0 = 0x00000081UL; /* веса AXI-очереди A */
SEMC->BMCR1 = 0x00000081UL; /* веса AXI-очереди B */
/* Базовые регистры регионов SEMC. BR0 — сама SDRAM (0x80000000, 32 МБ,
* VLD=1). BR1..BR8 прочие регионы из проверенного DCD, переносятся как
* есть (bootloader их не использует, но конфиг источника истины не режем). */
SEMC->BR[0] = 0x8000001BUL;
SEMC->BR[1] = 0x8200001BUL;
SEMC->BR[2] = 0x8400001BUL;
SEMC->BR[3] = 0x8600001BUL;
SEMC->BR[4] = 0x90000021UL;
SEMC->BR[5] = 0xA0000019UL;
SEMC->BR[6] = 0xA8000017UL;
SEMC->BR[7] = 0xA900001BUL;
SEMC->BR[8] = 0x00000021UL;
SEMC->IOCR = 0x000079A8UL; /* внутренний pinmux SEMC */
/* Геометрия и тайминги SDRAM. SDRAMCR0: PS=16бит, BL=8, COL=9бит, CL=3 —
* MT48LC16M16A2 (даташит: 512 колонок = 9 адресных бит). */
SEMC->SDRAMCR0 = 0x00000F31UL;
SEMC->SDRAMCR1 = 0x00652922UL;
SEMC->SDRAMCR2 = 0x00020201UL;
SEMC->SDRAMCR3 = 0x08193D0FUL; /* тайминги refresh; REN включим в конце */
/* DBICR0/DBICR1 — DCD их пишет, хотя DBI-устройства на плате нет и
* SEMC_ConfigureSDRAM() их не трогает. Инертны (регион DBI не включён),
* но переносятся дословно ради полного соответствия проверенному DCD. */
SEMC->DBICR0 = 0x00000021UL;
SEMC->DBICR1 = 0x00888888UL;
/* Параметры IP-команд: DATSZ=2 байта (запись mode-register 16-бит шиной). */
SEMC->IPCR1 = 0x00000002UL;
SEMC->IPCR2 = 0x00000000UL;
}
/* ── SEMC: командная последовательность инициализации SDRAM ──────── */
/**
* @brief Прогнать init-последовательность SDRAM через IP-команды SEMC.
*
* precharge-all 2×auto-refresh mode-set включение авто-refresh.
*
* @retval BSP_OK все команды завершились успешно.
* @retval BSP_ERR_INIT IP-команда SEMC вернула ошибку.
*/
static bsp_status_t sdram_issue_init_sequence(void)
{
const uint32_t ADDR = SDRAM_SEMC_IPCMD_ADDR;
if (SEMC_SendIPCommand(SEMC, kSEMC_MemType_SDRAM, ADDR, (uint32_t) kSEMC_SDRAMCM_Prechargeall,
0, NULL) != kStatus_Success)
{
return BSP_ERR_INIT;
}
for (uint32_t i = 0U; i < 2U; i++)
{
if (SEMC_SendIPCommand(SEMC, kSEMC_MemType_SDRAM, ADDR,
(uint32_t) kSEMC_SDRAMCM_AutoRefresh, 0, NULL) != kStatus_Success)
{
return BSP_ERR_INIT;
}
}
if (SEMC_SendIPCommand(SEMC, kSEMC_MemType_SDRAM, ADDR, (uint32_t) kSEMC_SDRAMCM_Modeset,
SDRAM_SEMC_MODE_REG, NULL) != kStatus_Success)
{
return BSP_ERR_INIT;
}
/* Включить авто-refresh + перейти на рабочие параметры refresh. DCD пишет
* SDRAMCR3 повторно другим значением (не только бит REN) переносим как
* есть; это финальный «operational» refresh-конфиг после инициализации. */
SEMC->SDRAMCR3 = 0x50210A09UL;
return BSP_OK;
}
/* ── SEMC: AXI-QoS арбитраж ─ */
/**
* @brief Настроить приоритеты доступа мастеров к SDRAM
*
* Требуют board_mpu_init() Region 11
*/
static void sdram_configure_axi_qos(void)
{
SDRAM_QOS_LCD_READ = 6UL;
SDRAM_QOS_LCD_WRITE = 6UL;
SDRAM_QOS_M7_READ = 7UL;
SDRAM_QOS_M7_WRITE = 7UL;
}
/* ── Внутренние функции верификации ────────────────────────────────────── */
/* ── Внутренние функции ────────────────────────────────────────────────── */
/**
* @brief Дождаться перехода SEMC в состояние IDLE.
@ -269,10 +72,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();
}
@ -284,33 +87,16 @@ 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();
@ -331,6 +117,6 @@ bsp_status_t bsp_sdram_init(void)
return BSP_ERR_INIT;
}
g_s_initialised = true;
s_initialised = true;
return BSP_OK;
}

View file

@ -44,16 +44,12 @@ 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()` возвращает фактически прочитанное количество байт —

View file

@ -26,7 +26,7 @@
/* -------------------------------------------------------------------------- */
/* Константы */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------s------------------------- */
/** Передать в timeout_ms чтобы ждать данные бесконечно. */
#define BSP_UART_HOST_WAIT_FOREVER (UINT32_MAX)

View file

@ -84,14 +84,17 @@ 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;
@ -188,6 +191,7 @@ size_t bsp_uart_host_read(uint8_t *p_buf, size_t len, uint32_t timeout_ms)
continue;
}
/* Буфер пуст — проверяем таймаут. */
if (timeout_ms == 0U)
{
break;

View file

@ -19,9 +19,7 @@ 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**: `0x1996` / `0x00AD` (`usb_device_descriptor.h`) — тот же
идентификатор, что `tools/service_tui/` (service-tui) использует для
детекта CDC-порта firmware_test (`SERVICE_CDC_VID`/`SERVICE_CDC_PID`).
**VID/PID**: `0x1234` / `0x0001` — placeholder, заменить на производственные.
---

View file

@ -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);

View file

@ -566,6 +566,8 @@ 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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -119,7 +119,7 @@ set(CMAKE_ASM_FLAGS_RELEASE
CACHE INTERNAL "")
# -----------------------------------------------------------------------------
# C runtime библиотека newlib-nano
# C runtime библиотека newlib-nano (меньше размер, подходит для embedded)
# -----------------------------------------------------------------------------
# --specs=nano.specs использовать newlib-nano: облегчённая stdlib,
# меньший размер printf/malloc -Wl,--no-warn-rwx-segments подавить

View file

@ -1,198 +0,0 @@
# CI/CD: устройство GitHub Actions workflow
> Проект: TFT Firmware (MIMXRT1052CVJ5B)
> Документ описывает схему и принцип работы двух GitHub Actions workflow в
> репозитории: `ci.yml` (обычный PR/push-цикл) и `release.yml` (публикация
> релизных бинарников по тегу). Это техническая справка «как оно работает
> сейчас», а не хронология решений.
---
## 1. Два workflow, два разных назначения
| | `ci.yml` | `release.yml` |
| --- | --- | --- |
| Когда запускается | `push` в `dev`/`main`, любой `pull_request`, `workflow_dispatch` | `push` тега `tui-v*` / `firmware-v*` / `bootloader-v*`, `workflow_dispatch` |
| Что проверяет | Собирается ли проект и проходят ли host-тесты | Собираются ли и публикуются ли релизные бинарники |
| Публикует что-то наружу? | Нет — только артефакты прогона (для отладки) | Да — GitHub Release с реальными asset'ами (только по тегу) |
| Раннеры | `ubuntu-latest` (оба job'а) | `ubuntu-latest` + `macos-latest` + `windows-latest` |
Они намеренно не смешаны в один файл: PR-цикл должен оставаться быстрым и не
зависеть от кросс-платформенной упаковки `service-tui`, а релизная
публикация не должна гонять host-тесты повторно на каждый push в PR.
---
## 2. `ci.yml` — обычный PR/push-цикл
```mermaid
flowchart LR
trigger["push (dev/main)\nPR\nworkflow_dispatch"] --> build["build\nсборка в devcontainer"]
build -->|"artifact: build-tree"| test["test\nhost-тесты в devcontainer"]
```
Оба job'а выполняются на `ubuntu-latest`, внутри одного и того же
devcontainer-образа (`.devcontainer/Dockerfile`) — то же окружение, что и у
разработчика локально (ARM toolchain, cmake, ninja, `just`, `uv`), не
отдельно собранное под раннер. Образ пересобирается в каждом job'е, но
кэшируется через `docker/build-push-action@v7` (`cache-from`/`cache-to:
type=gha, scope=tft-devcontainer`) — повторные прогоны переиспользуют слои,
не пересобирают с нуля.
- **`build`** — checkout, поднять devcontainer, `tools/host && uv sync`,
`just ci::build` (→ `just build::build-all-release`, все три firmware-
проекта), выгрузить `build/` как артефакт `build-tree`.
- **`test`** — зависит от `build` (`needs: build`), скачивает `build-tree`,
поднимает тот же образ (тот же кэш), `just ci::test` (→
`just build::test-host-release`, Unity/fff host-тесты), выгружает логи +
`build/` как `test-artifacts`.
Команды внутри контейнера запускаются `docker run --user root -v
"$GITHUB_WORKSPACE":/workspace -w /workspace tft-devcontainer-ci:latest
bash -lc '...'` — `--user root` обязателен, иначе non-root пользователь в
контейнере не может писать в bind-mounted `$GITHUB_WORKSPACE` (ломало
`uv sync`/создание `.venv`).
Чего `ci.yml` **не делает**: lint (заглушка в `just ci::lint`), coverage,
сборку/упаковку `service-tui`, HIL-тесты (нужно физическое железо —
самостоятельная задача для self-hosted раннера). См. §5 ниже.
---
## 3. `release.yml` — публикация релиза
### 3.1 Схема тегов
Firmware (`firmware_test`), bootloader и `service-tui` версионируются и
релизятся **независимо** — три разных паттерна тега запускают три разных
сценария внутри одного workflow-файла:
```mermaid
flowchart TD
push_fw["push tag\nfirmware-vX.Y.Z"] --> firmware
push_bl["push tag\nbootloader-vX.Y.Z"] --> firmware
push_tui["push tag\ntui-vX.Y.Z"] --> firmware
dispatch["workflow_dispatch\n(release_type: tui | firmware | bootloader)"] --> firmware
firmware["firmware\n(ubuntu-latest, devcontainer)\nhab-firmware-test-debug +\nhab-bootloader-release"]
firmware -->|"тег firmware-v*"| publishFw["publish-firmware\ngh release create\n(HAB Debug)"]
firmware -->|"тег bootloader-v*"| publishBl["publish-bootloader\ngh release create\n(HAB Release, тестовый ключ)"]
firmware -->|"тег tui-v* ИЛИ\ndispatch(tui)"| macos["service-tui-macos\njust host::package-tui"]
firmware -->|"тег tui-v* ИЛИ\ndispatch(tui)"| windows["service-tui-windows\njust host::package-tui"]
macos -->|"тег tui-v*"| publishTui["publish-tui\ngh release create\n(2× .zip)"]
windows -->|"тег tui-v*"| publishTui
```
Ключевое архитектурное решение: **HAB-образы `firmware_test` и `bootloader`,
которые вшиваются внутрь TUI-бандла, всегда собираются заново из текущего
HEAD** джобой `firmware`а не скачиваются из последних опубликованных
`firmware-v*`/`bootloader-v*` релизов. Поэтому job `firmware` выполняется
**при любом триггере**, без условия — она нужна и для отдельных релизов
firmware_test/bootloader, и как общая зависимость для упаковки TUI (см.
Фазу 5, `firmware/bootloader/PLAN.md` — до неё `firmware` собирала только
firmware_test, и TUI-бандл молча уходил без образа bootloader).
### 3.2 Триггеры
```yaml
on:
push:
tags: ["tui-v*", "firmware-v*", "bootloader-v*"]
workflow_dispatch:
inputs:
release_type: {type: choice, options: [tui, firmware, bootloader], default: tui}
```
`workflow_dispatch` — «сухой прогон» без публикации: собирает всё
(включая упаковку TUI под выбранный `release_type`), выгружает скачиваемые
артефакты, но **не** создаёт GitHub Release — джобы `publish-*` гейтятся
условием `if: startsWith(github.ref, 'refs/tags/...')`, которое на ручном
запуске всегда ложно (`github.ref` в этом случае — ветка, не тег).
Важный нюанс реализации: условие для `service-tui-macos`/`-windows`
проверяет `release_type` через `github.event.inputs.release_type`, не
через голый контекст `inputs.release_type` — последний рассчитан прежде
всего на reusable workflows (`workflow_call`) и не даёт предсказуемого
результата в job-level `if:` для прямого `workflow_dispatch`. На первом
реальном прогоне (`inputs.release_type`) джобы `service-tui-*` молча
скипались даже при выбранном `release_type=tui` — потребовалась замена на
`github.event.inputs.*`, если снова понадобится ссылаться на inputs в
job-level `if:`, использовать именно эту форму.
### 3.3 Джобы
| Job | Раннер | Когда выполняется | Что делает |
| --- | --- | --- | --- |
| `firmware` | `ubuntu-latest`, devcontainer (тот же подход и кэш, что в `ci.yml`) | всегда | сверяет тег `firmware-v*` с `VERSION` в `firmware/test/CMakeLists.txt` (если применимо); `just build::hab-firmware-test-debug` → артефакт `firmware-hab-debug`; сверяет тег `bootloader-v*` с `VERSION` в `firmware/bootloader/CMakeLists.txt` (если применимо); `just build::hab-bootloader-release` → артефакт `bootloader-hab-release` |
| `publish-firmware` | `ubuntu-latest` | только push тега `firmware-v*` | скачивает `firmware-hab-debug`; `gh release create firmware-vX.Y.Z firmware_test_hab.bin` — standalone-релиз для `tools/host/flash_usb.py`, без TUI |
| `publish-bootloader` | `ubuntu-latest` | только push тега `bootloader-v*` | скачивает `bootloader-hab-release`; `gh release create bootloader-vX.Y.Z bootloader_hab.bin` — standalone-релиз (USB ROM/SWD, без TUI); `--notes` явно предупреждает, что HAB-подпись тестовым ключом, не production (см. `firmware/bootloader/SIGNING_CEREMONY.md`) |
| `service-tui-macos` / `service-tui-windows` | `macos-latest` / `windows-latest` | push тега `tui-v*` ИЛИ `workflow_dispatch` с `release_type=tui` | `astral-sh/setup-uv` + `extractions/setup-just` (на раннерах нет `uv`/`just` из коробки); скачивает `firmware-hab-debug` в `build/Debug/` и `bootloader-hab-release` в `build/Release/`; сверяет тег `tui-v*` с `version` в `pyproject.toml` (если применимо); `just host::service-setup` + `just host::package-tui` (падает явно, если `build/Release/bootloader_hab.bin` не найден — production-функция TUI жёстко требует именно этот файл); архивирует `dist/service-tui-vX.Y.Z-<os>/` в zip (`zip -r` на macOS, `Compress-Archive` на Windows); артефакт `service-tui-macos`/`service-tui-windows` |
| `publish-tui` | `ubuntu-latest`, `needs: [service-tui-macos, service-tui-windows]` | только push тега `tui-v*` | скачивает оба zip; `gh release create tui-vX.Y.Z *.zip` |
**Debug HAB для firmware_test, Release HAB для bootloader** — не единое
правило «всегда Debug». Release-сборка `firmware_test` нестабильна
(FCB/clock), поэтому `firmware`-джоба собирает `hab-firmware-test-debug`,
не полный `hab-all-release`. Bootloader — наоборот: production-путь (Фаза 5,
`firmware/bootloader/PLAN.md`) жёстко требует **Release**, подписанный
(`flags=0x08`) — Debug-конфиг bootloader остаётся unsigned и используется
только для локальной отладки, в релиз/TUI-бандл не попадает.
**Сверка версии тег↔файл** — маленький, но важный guard в обеих ветках
(`firmware`/`service-tui-*`): если версия в теге не совпадает с версией в
`CMakeLists.txt`/`pyproject.toml`, job падает с понятной ошибкой вместо
того, чтобы молча опубликовать релиз с несовпадающим номером версии внутри
файлов (забытый version bump перед тегом).
**Windows-раннер и bash** — корневой `Justfile` требует `bash` (`set shell
:= ["bash", ...]`); `windows-latest` образ GitHub Actions включает Git for
Windows (даёт `bash.exe` в PATH из коробки) — дополнительной настройки
shell не требуется, `just`-рецепты выполняются так же, как и локально под
Git Bash на Windows.
### 3.4 Как проверить без публикации
```bash
gh workflow run release.yml --ref dev -f release_type=tui
gh workflow run release.yml --ref dev -f release_type=firmware
gh workflow run release.yml --ref dev -f release_type=bootloader
```
или через веб-интерфейс: Actions → **Release****Run workflow** → выбрать
branch и `release_type`. Джобы `publish-*` в этом сценарии показываются как
**Skipped**, не **Failed** — это ожидаемое поведение, не баг.
---
## 4. Общее между `ci.yml` и `release.yml`
- **Один и тот же devcontainer-подход** для всего, что требует ARM
toolchain (сборка firmware) — job `firmware` в `release.yml` использует
дословно тот же паттерн `docker/build-push-action@v7` +
`docker run --user root ...`, что и `build`/`test` в `ci.yml`.
- **Общий GHA layer-кэш**оба workflow используют `scope:
tft-devcontainer` в `cache-from`/`cache-to`, поэтому кэш переиспользуется
между обычными PR-прогонами и релизными сборками, а не живёт отдельно.
- **`workflow_dispatch` есть у обоих** — в `ci.yml` это просто способ
перезапустить pipeline вручную без нового коммита; в `release.yml` — это
единственный способ протестировать сборку без реальной публикации.
---
## 5. Чего пока нет
- **Self-hosted HIL-раннер.** Ни один из двух workflow не может
задетектировать SDP на живой плате или прогнать деструктивные сценарии
(обрыв USB) — GitHub-hosted раннеры не видят реальное железо. Это ручной
шаг перед каждым релизом, пока не поднят self-hosted lane.
- **`lint`/`coverage`** не подключены ни в `ci.yml`, ни в `release.yml`.
- **Публикация devcontainer image в GHCR** — образ пересобирается в каждой
job'е каждого workflow (пусть и с layer-кэшем); заранее опубликованный
образ сократил бы время старта ещё сильнее.
- **`bootloader-v*`/`tui-v*` релизы несут HAB-образ, подписанный ТЕСТОВЫМ
ключом** (`tools/host/hab/keys/`, HAB Open, схема NOCAK) — не production.
Реальная SRK-церемония описана в
`firmware/bootloader/SIGNING_CEREMONY.md`, в CI пока не встроена (сама
церемония — не автоматизируемый процесс, см. документ).

View file

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

View file

@ -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 0xC0000207\nинициализация FlexSPI NOR + QE-бит"]
C --> D["configure-memory 0xC0000007\nинициализация FlexSPI NOR"]
D --> E["flash-erase-region 0x60000000"]
E --> F["configure-memory 0xF000000F\nзапись FCB в 0x60000000"]
F --> G["write-memory 0x60001000\nHAB-образ"]
@ -79,23 +79,18 @@ auto-config не подтверждена — см. 1.5.
### 1.5 Нестандартная память (W25Q256/512) и сторонние бинарники
`service-tui` (`tools/service_tui/`) умеет прошивать бинарники, собранные не
`service-tui` (`tools/production/`) умеет прошивать бинарники, собранные не
в этом репозитории (например, старые платы с W25Q512), тем же способом
(USB SDP), но с двумя отличиями от штатного пути. Это **отдельная
реализация**, не связанная с `flash_usb.py`/`nxpimage` CLI — TUI прошивает
in-process через Python API `spsdk` (`app/flash_backend.py`: `HabImage`,
`McuBoot`, `SDP`), без единого subprocess:
(USB SDP), но с двумя отличиями от штатного пути:
- HAB-образ (IVT + опционально DCD) собирается из **сырого** бинарника на
лету через `HabImage` (spsdk), а не заранее через `just build::hab-*`
- FCB пишется **явно** (`mboot.write_memory()` с готовым блобом
`tools/host/dcd/w25qXXX_fdcb.bin`, буквальная запись вместо
`configure-memory 0xF000000F`) — auto-config для 4-байтной адресации не
проверялся, решили на него не полагаться
лету через `nxpimage`, а не заранее через `just build::hab-*`
- FCB пишется **явно** (`flash_usb.py --fcb-path tools/host/dcd/w25qXXX_fdcb.bin`,
буквальный `write-memory` вместо `configure-memory 0xF000000F`) — auto-config
для 4-байтной адресации не проверялся, решили на него не полагаться
Подробности конвейера — в [tools/service_tui/docs/ARCHITECTURE.md](../tools/service_tui/docs/ARCHITECTURE.md),
§8. Штатный путь (`--firmware`, три сборки этого репозитория, что через
`just host::flash`, что через `service-tui`) не меняется и по-прежнему
Подробности конвейера — в `tools/production/DEV_ARCH.md`, §8. Штатный путь
(`--firmware`, три сборки этого репозитория) не меняется и по-прежнему
использует auto-config Flashloader, как описано в 1.4.
---

View file

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

View file

@ -1,302 +0,0 @@
# Загрузчик TFT — принцип работы
Документ описывает поведение загрузчика: как устроена память, как выбирается и обновляется образ
приложения, правила версий, даунгрейд и режим восстановления.
---
## 1. Карта памяти
Приложение хранится в QSPI NOR Flash в **двух слотах** — A и Б. Каждый слот содержит **полную,
самостоятельно валидную** копию приложения. Загрузчик занимает начало flash, за слотами идёт
отдельная область под ассеты (спрайты, звук), которая к процессу загрузки отношения не имеет.
| Область | Начало | Размер |
| ------------------------ | ------------ | ------------- |
| Загрузчик | `0x60000000` | 256 КБ |
| Slot A | `0x60040000` | 2 МБ |
| Slot Б | `0x60240000` | 2 МБ |
| Файловая система ассетов | `0x60440000` | до конца чипа |
```mermaid
flowchart TB
BL["Загрузчик — 256 КБ<br/>0x60000000"]
SA["Slot A — 2 МБ<br/>0x60040000"]
SB["Slot Б — 2 МБ<br/>0x60240000"]
FS["Файловая система ассетов<br/>0x60440000 … конец чипа"]
BL --- SA --- SB --- FS
```
**Приложение исполняется прямо из flash** (XIP) — из того слота, который выбрал
загрузчик, без копирования в ОЗУ. Из этого следует ключевое свойство: образ жёстко привязан к адресу
своего слота при сборке. Поэтому **релиз приложения — это два бинарника на одну версию**: один собран
под адрес Slot A, второй — под адрес Slot Б. Образ, физически положенный не в «свой» слот, пройдёт
проверку подписи, но не запустится.
Два слота нужны для безопасного обновления: пока приложение работает из одного слота, новый образ
пишется в другой. Рабочая копия никогда не затирается — на диске всегда есть чем загрузиться, даже
если обновление прервётся на середине.
---
## 2. Выбор образа при старте
На каждой подаче питания загрузчик решает, из какого слота запускать приложение.
**Слот считается кандидатом, только если он валиден целиком**: корректная сигнатура формата, целый
хэш содержимого и верная криптографическая подпись (ECDSA-P256). Битый или неподписанный слот
игнорируется.
Правила выбора:
- Оба слота валидны → активным становится слот с **большей версией**.
- Валиден только один → он и активен.
- Ни одного валидного → активного слота нет, загрузчик переходит в ожидание microSD.
```mermaid
flowchart TD
START([Подача питания]) --> CHECK["Проверить оба слота:<br/>сигнатура + хэш + подпись"]
CHECK --> CMP{Сколько валидных?}
CMP -->|Оба| HIGHER["Активный = слот<br/>с большей версией"]
CMP -->|Один| ONE["Активный = он"]
CMP -->|Ни одного| NONE["Ожидание microSD"]
HIGHER --> JUMP([Запуск приложения])
ONE --> JUMP
```
**Защитная сеть от битого обновления.** Свежеустановленный образ считается «непроверенным», пока сам
не подтвердит своё здоровье в рантайме. Если непроверенный образ запустился, но так и не подтвердился
(например, завис на старте) — при следующей загрузке он трактуется как неудавшееся обновление и
**автоматически стирается**, а загрузчик откатывается на прежний слот. Приложение обязано подтвердить
себя один раз, доказав работоспособность.
---
## 3. Обновление через microSD
**Единственный полевой канал обновления — карта microSD.** Загрузчик ищет в корне карты файл
`TFT_APP.BIN` — подписанный образ приложения. Карта проверяется при старте и периодически (примерно
раз в 1.5 с), пока загрузчик находится в ожидании, — карту можно вставить уже после включения.
Приёмка кандидата — **двухступенчатая**:
1. **Проверка заголовка** (сигнатура формата + версия) прямо с карты, до касания flash — этого
достаточно, чтобы решить «ставить или пропустить».
2. **Полная криптографическая проверка** — уже после записи в целевой слот. Если подпись битая,
только что записанный слот просто не будет выбран при загрузке, и загрузчик останется на прежнем
валидном образе.
Запись идёт **потоком по частям**, каждая записанная часть немедленно вычитывается обратно и
сверяется — битая страница ловится сразу.
**Целевой слот установки — всегда НЕ активный.** Работающий/загружаемый слот не перезаписывается
никогда. Если активного слота нет вообще (чистая плата) — по умолчанию Slot A.
```mermaid
flowchart TD
CARD([microSD + TFT_APP.BIN]) --> HDR["Пик заголовка:<br/>сигнатура + версия"]
HDR -->|Сигнатура битая| REJ1["Отклонить<br/>(candidate invalid)"]
HDR -->|OK| DEC{Решение по версии}
DEC -->|Ставить| WRITE["Записать в НЕактивный слот:<br/>стереть → поток + verify"]
DEC -->|Пропустить| SKIP["Пропустить<br/>(update skipped)"]
WRITE --> CRYPTO{"Крипто-проверка<br/>записанного слота"}
CRYPTO -->|OK| DONE([Установлено → загрузка])
CRYPTO -->|Подпись битая| REJ2["Отклонить<br/>(install rejected)"]
```
**Заводской сценарий** — частный случай этой же логики: чистая плата с одним загрузчиком, оба слота
пусты. Загрузчик ждёт SD, при появлении `TFT_APP.BIN` ставит его в Slot A и загружается. Никакого
отдельного механизма первичной заливки нет.
---
## 4. Правила версий
Версия образа — **major.minor.revision**. Номер сборки (build) в сравнении **не участвует**: два
образа, отличающиеся только номером сборки, считаются равными.
Версия используется дважды:
- при **выборе** активного слота — побеждает бо́льшая версия;
- при **решении об установке** кандидата с SD.
Решение по кандидату (без удержания кнопки):
| Кандидат относительно активного | Действие |
| ------------------------------- | ---------------------------- |
| Строго новее | Установить в неактивный слот |
| Активного слота нет вообще | Установить в Slot A |
| Старше или равен | Пропустить |
При штатном обновлении (кандидат новее) прежний активный слот **не стирается** — он естественным
образом проиграет сравнение версий при следующей загрузке, новый образ победит сам.
---
## 5. Даунгрейд
Установить образ **старее** уже стоящего можно только с помощью оператора: **удержать `BTN 1` в
момент подачи питания**. Кнопка считывается один раз на старте и действует всю сессию.
Ключевой момент: при обычном даунгрейде записать старый образ в свободный слот **недостаточно**
прежний (более новый) активный слот остался бы валиден и снова победил бы по версии, и даунгрейд
физически лёг бы на flash, но не загрузился. Поэтому при форсированном даунгрейде прежний активный
слот **стирается** — но только **после** того, как новый образ уже записан и подтверждён валидным.
На диске никогда не бывает нуля рабочих слотов даже на середине операции.
| Условие | Действие |
| ---------------------------------- | ----------------------------------------------------- |
| Кандидат старше + `BTN 1` удержана | Установить в свободный слот, стереть прежний активный |
| Кандидат равен активному + `BTN 1` | Пропустить (переустановку той же версии не форсируем) |
---
## 6. Режим восстановления (Recovery Mode)
Назначение — не дать полевой плате превратиться в «кирпич», если уже установленный образ зависает в
рантайме, и дать оператору ручной аварийный вход.
### Аппаратный сторож
Плата защищена аппаратным watchdog с таймаутом **10 секунд**. Если управление зависает где-либо
(включая рантайм приложения), через 10 с происходит аппаратный сброс. Watchdog взводится один раз и
до перезагрузки по питанию не выключается — он «переживает» переход в приложение, поэтому приложение
обязано периодически его «кормить». Зависание → гарантированный сброс, а не вечный локап.
### Счётчик и порог
Число **подряд идущих** watchdog-сбросов хранится в регистре, который переживает тёплый/watchdog-сброс
и обнуляется только при настоящей подаче питания (POR). Счётчик обнуляется также при успешной
установке нового образа и при откате на фолбэк. Порог срабатывания — **3** сброса подряд.
### Классификация отказов и их обработка
| Класс | Ситуация | Что срабатывает |
| ----- | ----------------------------------------------- | ----------------------------------------------------- |
| **A** | Новый образ завис, ещё не подтвердив себя | Watchdog-сброс + автоматический откат на прежний слот |
| **B** | Уже подтверждённый образ завис в рантайме | Счётчик сбросов достиг порога → фолбэк на второй слот |
| **C** | Откатываться некуда (единственный/оба зависают) | Recovery Mode |
| **D** | Оператор хочет чистый старт вручную | Recovery Mode по `BTN 2` |
Класс A — самый частый — закрыт полностью автоматически: откат непроверенного образа не требует ни
счётчика, ни вмешательства. Класс B ловит то, что откат не покрывает (образ-то подтверждён): после
порога зависший слот стирается, и загружается второй, если он валиден.
### Решение при старте
```mermaid
flowchart TD
S([Начало попытки]) --> BTN{BTN 2 удержана?}
BTN -->|Да| REC[Recovery Mode]
BTN -->|Нет| CNT{"Счётчик сбросов<br/>≥ порога (3)?"}
CNT -->|Нет| NORM[Обычная загрузка]
CNT -->|Да| FB{Второй слот валиден?}
FB -->|Да| ERASE["Стереть зависший слот →<br/>обнулить счётчик →<br/>загрузить второй"]
FB -->|Нет| REC
```
`BTN 2` проверяется **первым** — приоритет ручного входа выше и счётчика, и обычной загрузки, и
кнопки даунгрейда. Если `BTN 2` удержана, обычный путь загрузки не выполняется вообще, даже при
наличии валидного образа.
### Поведение в Recovery Mode
- **Прыжок в приложение подавлен** — это само по себе разрывает цикл зависаний.
- **Отдельная LED-индикация**: оба светодиода мигают синхронно, 100 мс включено / 100 мс выключено —
явно отличается от heartbeat и рабочих паттернов приложения.
- **Статус по USB**: `recovery_mode`.
- **Ослабленный контроль версий**: принимается **любой** подписанный образ с SD — без сравнения
версий и без кнопки. Проверка подписи при этом сохраняется всегда.
- При найденном валидном образе — **оба слота стираются**, образ ставится в Slot A, происходит
автоматический прыжок. «Чистый борт» достигается ровно тогда, когда есть чем заменить.
### Семантика «на одну сессию»
Счётчик не сохраняется во flash. На подаче питания он обнуляется, поэтому зависший слот **пробуется
заново** — если зависание было случайным (транзиентным), плата получает новый шанс. Если зависание
детерминированное, оператор жмёт `BTN 2` и входит в recovery немедленно, не дожидаясь порога.
### Честная граница
Если образ стабильно работает, обнуляет счётчик (доказав здоровье), и лишь **потом** ловит редкий баг
(конкретный файл на SD, конкретное входное сообщение) — счётчик каждый раз обнуляется до зависания,
автопорог не накапливается, и цикл автоматически не ловится. Это принципиально: по таймеру не отличить
«здоров» от «здоров, но потом словил редкое». В таком случае плата видимо циклится (watchdog +
recovery-индикация это показывают), лечится SD-фиксом или `BTN 2`. Watchdog как минимум не даёт плате
зависнуть намертво.
---
## 7. Индикация и обратная связь
### Светодиоды
Полный словарь (установка, «железо не в порядке», ошибка образа, приоритет между ними) —
[LED_PATTERNS.md](LED_PATTERNS.md). Кратко:
| Состояние | Паттерн |
| ---------------------- | ------------------------------------------------ |
| Загрузчик жив, ждёт SD | Один LED: короткий импульс ~50 мс, пауза ~450 мс |
| Приложение работает | Задаётся приложением |
| Recovery Mode | Оба LED синхронно: 100 мс вкл / 100 мс выкл |
### USB (виртуальный COM-порт)
Загрузчик поднимает USB-порт до обращения к SD, поэтому статусы видны, даже если оператор подключился
заранее. Обмен — текстовые JSON-строки.
Состояния (`status`):
| Значение | Когда |
| ---------------- | ------------------------------------------------- |
| `waiting_for_sd` | Нет валидного слота, ждём карту |
| `installing` | Принято решение установить кандидата, идёт запись |
| `update_skipped` | Кандидат отклонён по версии |
| `recovery_mode` | Плата в режиме восстановления |
Ошибки установки: `SD_CANDIDATE_INVALID` (битый заголовок), `SD_INSTALL_WRITE_FAILED` (сбой записи),
`SD_INSTALL_REJECTED` (записан, но подпись не прошла), `SD_DOWNGRADE_ERASE_FAILED`.
Статус сторожа (по запросу `wdog`): взведён ли watchdog, таймаут, был ли последний сброс по watchdog,
текущее значение счётчика сбросов и порог.
---
## 8. Полный жизненный цикл
```mermaid
stateDiagram-v2
state "Загрузка" as BOOT
state "Приложение" as APP
state "Ожидание SD" as WAIT
state "Установка" as INST
state "Recovery Mode" as REC
[*] --> BOOT: питание
BOOT --> APP: валидный образ выбран
BOOT --> WAIT: нет валидного слота
BOOT --> REC: порог сбросов / BTN 2
WAIT --> INST: TFT_APP.BIN найден
INST --> APP: установлено + прыжок
INST --> WAIT: отклонено по версии/подписи
APP --> BOOT: watchdog-сброс при зависании
REC --> INST: образ с SD (любая подписанная версия)
REC --> REC: ждём SD
```
---
## 9. Сводка сценариев
| Ситуация | Поведение загрузчика |
| ------------------------------------------------------ | --------------------------------------------------------------------- |
| Оба слота валидны | Загрузка слота с бо́льшей версией |
| Валиден один слот | Загрузка его |
| Чистая плата, оба слота пусты | Ожидание SD (бессрочно), heartbeat |
| SD с образом новее активного | Установка в свободный слот → загрузка |
| SD с образом старше/равным, кнопка не нажата | Пропуск, загрузка прежнего |
| SD с образом старше, `BTN 1` удержана | Даунгрейд: установка + стирание прежнего активного → загрузка старого |
| SD с битым заголовком / битой подписью | Отклонение, прежний валидный слот не тронут |
| Новый образ завис, не подтвердившись (Класс A) | Watchdog-сброс → автоматический откат на прежний слот |
| Подтверждённый образ завис, есть второй слот (Класс B) | 3 сброса → стирание зависшего слота → загрузка второго |
| Зависает, откатываться некуда (Класс C) | Recovery Mode |
| Оператор удержал `BTN 2` при старте (Класс D) | Recovery Mode немедленно, обычная загрузка подавлена |
| В recovery вставлена SD с подписанным образом | Оба слота стёрты, образ в Slot A, автоматический прыжок |
| POR после recovery без `BTN 2` | Счётчик обнулён, зависший слот пробуется заново |

View file

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

View file

@ -85,7 +85,7 @@ typedef struct {
DCD содержит последовательность команд двух типов:
| Команда | Тег | Назначение |
| ---------------- | ------ | -------------------------------- |
|---|---|---|
| `Write Data` | `0xCC` | Записать значение по адресу |
| `Check Bits Set` | `0xCF` | Ждать пока бит станет `1` (poll) |
@ -106,7 +106,7 @@ DCD содержит последовательность команд двух
### 4.1 По типу образа
| Режим | `flags` | CSF | Применение |
| ------------------ | ------- | ------------------------- | ------------------- |
|---|---|---|---|
| Unsigned | `0x00` | отсутствует | разработка, отладка |
| Signed | `0x08` | RSA/ECDSA подпись | производство |
| Signed + Encrypted | `0x0C` | подпись + шифрование кода | максимальная защита |
@ -150,42 +150,6 @@ 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-контейнер
@ -206,7 +170,7 @@ export`, `flags`, `AuthenticateData`) не меняется, меняются т
BootROM в режиме SDP умеет только писать в RAM и прыгать. Для записи во Flash необходим **Flashloader** — специальная программа от NXP.
```bash
```
Плата в SDP режиме (BOOT_MOD_1 = 3V3)
│ sdphost -u 0x1FC9,0x0130
@ -217,7 +181,7 @@ BootROM в режиме SDP умеет только писать в RAM и пр
Flashloader запущен
│ blhost -u 0x15A2,0x0073
├─ fill-memory 0x2000 4 0xC0000207 ← option word для FlexSPI NOR (+QE-бит)
├─ fill-memory 0x2000 4 0xC0000007 ← option word для FlexSPI NOR
├─ configure-memory 9 0x2000 ← Flashloader пишет FCB в Flash
├─ flash-erase-region 0x60000000 …
├─ write-memory 0x60002000 firmware_hab.bin ← HAB образ
@ -229,7 +193,7 @@ BootROM в режиме SDP умеет только писать в RAM и пр
## 8. Жизненный цикл для проекта TFT
| Стадия | Режим HAB | Подпись | Fuse |
| --------------------- | ---------- | ----------------- | ---------------- |
|---|---|---|---|
| Разработка | Open | Unsigned (`0x00`) | не трогаем |
| Входной контроль | Open | Unsigned (`0x00`) | не трогаем |
| Предсерийные образцы | Open | Signed (`0x08`) | не трогаем |
@ -242,7 +206,7 @@ BootROM в режиме SDP умеет только писать в RAM и пр
## 9. Инструменты
| Инструмент | Назначение |
| --------------------- | ---------------------------------------------------------- |
|---|---|
| `nxpimage hab export` | Сборка HAB-образа (IVT + BDT + DCD + опционально CSF) |
| `nxpimage hab parse` | Разбор готового образа для проверки |
| `sdphost` | Связь с BootROM в SDP режиме (запись в RAM, прыжок) |

View file

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

View file

@ -4,13 +4,7 @@
>
> Документ описывает протокол обмена между диагностической прошивкой
> (`firmware_test`) и хостовым ПО сервисного инженера.
> Актуален для: `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), а не полное описание тест-логики.
> Актуален для: `firmware_test v0.1.0+`, `protocol.h v2`.
---
@ -85,7 +79,7 @@ sequenceDiagram
participant T as Таргет
Note over T: прошивка загружена через USB SDP
T-->>H: {"type":"session_start","fw":"0.1.2","target":"IMXRT1052","uptime_ms":0}
T-->>H: {"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
H->>T: {"type":"cmd","cmd":"ping"}
T-->>H: {"type":"pong"}
@ -175,17 +169,6 @@ 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. Порядок выполнения — по реестру таргета,
@ -216,7 +199,7 @@ sequenceDiagram
### `session_start`
```json
{"type":"session_start","fw":"0.1.2","target":"IMXRT1052","uptime_ms":0}
{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
```
### `test_begin`
@ -239,11 +222,6 @@ sequenceDiagram
`detail` — ASCII-строка до 95 символов. При `pass` — пустая.
### `test_list`
Ответ на `list_tests` — массив дескрипторов теста (`id`, `name`,
`critical`, `requires_hil`), см. пример в разделе `list_tests` выше.
### `progress`
```json
@ -252,11 +230,6 @@ sequenceDiagram
Промежуточные шаги внутри теста. Используется в `usd`.
### `uid_response` / `version_response`
Ответы на `get_uid`/`get_version` — см. описание соответствующих команд
выше.
### `confirm_request`
```json
@ -297,30 +270,25 @@ sequenceDiagram
| `UNKNOWN_TEST` | Поле `"id"` в `run` или `"tests"` в `run_selected` содержит неизвестный ID |
| `LINE_TOO_LONG` | Входящая строка превысила 128 байт |
| `BUSY` | Таргет выполняет тест, новая команда отклонена |
| `UID_READ_ERR` | `bsp_prov_read_uid()` вернул ошибку (ответ на `get_uid`) |
---
## Матрица тестов
Порядок — как в реестре `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 | ❌ | ❌ | ✅ (слышимость тона) |
| `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 | ❌ | ✅ | ❌ |
**Типы:** **self** — таргет тестирует периферию самостоятельно; **interactive**
требует `confirm_request`, отвечает оператор; **HIL** — требует M5StampPLC,
confirm автоматический (без оператора).
требует `confirm_request`; **HIL** — требует M5StampPLC.
---
@ -352,10 +320,6 @@ sequenceDiagram
### Display (RGB888)
Шесть шагов: Red → Green → Blue → White, затем два ротационных (диагностика
непропаянных LR/UD пинов на TFT7/8/10). Тест прерывается на **первом**
неподтверждённом шаге.
```mermaid
sequenceDiagram
participant H as Хост
@ -369,36 +333,10 @@ 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":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":""}
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"}
```
При отказе/таймауте на любом шаге: `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
@ -455,15 +393,32 @@ firmware/test/src/
├── test_usd.c
├── test_display.c
├── test_buttons.c
├── test_opto.c
├── test_can.c
└── test_mqs.c
├── test_uart_ttl.c
├── test_uart_iso.c
└── test_opto.c
```
### Добавление нового теста
Пошаговый гайд с шаблонами (self-тест, интерактивный, pre-confirm) —
[firmware/test/README.md §Как добавить новый тест](../../firmware/test/README.md#как-добавить-новый-тест).
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` таргета.
---

View file

@ -166,18 +166,12 @@ ls build/target-debug/tests/target/<name>/test_<name>.elf
## Шаг 5 — `conftest.py`: добавить фикстуры
### Любой тест — фикстура загрузки всегда зависит от `m5`
M5StampPLC управляет питанием таргета (RLY1 → VIN, см. `HIL_BENCH.md`), а
не только сигнальными реле — поэтому `loaded_<n>` зависит от `m5` **во всех
случаях**, даже если сам тест не использует реле для сигналов (например,
`01_test_uart.py`/`loaded_host_uart`). Без этой зависимости pyOCD попытается
подключиться к обесточенной плате.
### Базовый тест (без M5)
```python
# 1. Фикстура загрузки — m5 гарантирует, что питание включено до pyOCD
# 1. Фикстура загрузки
@pytest.fixture(scope="module")
def loaded_<n>(request: pytest.FixtureRequest, m5: M5Agent) -> None:
def loaded_<n>(request: pytest.FixtureRequest) -> None:
_load_elf(
request,
Path(cfg.BUILD_DIR) / "tests/target/<n>/test_<n>.elf",
@ -190,10 +184,26 @@ _UART_FIXTURE_MAP = {
}
```
Различие между «базовым» и «с M5» тестом — не в сигнатуре `loaded_<n>`
(она всегда одна и та же), а в том, использует ли сам **тест-кейс**
`m5.opto_set()`/`m5.relay_set()`/`m5.can_*()` для управления сигналами
помимо включения питания (см. пример «Тест с M5» в Шаге 6 ниже).
### Тест с 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 попытается подключиться до включения питания.
---

View file

@ -1,193 +1,232 @@
# Добавление нового host unit-теста
# Отладка прошивок через SWD + GDB
Пошаговый гайд для разработчика. Полный справочник по Unity/FFF API,
структуре stub-хедеров и типичным ловушкам — в
[tests/host/README.md](../../../tests/host/README.md). Этот документ —
только про шаги добавления нового теста в сборку.
## Обзор архитектуры
---
## Обзор стека
Отладка построена на пробросе GDB-сервера с хоста в devcontainer по TCP. Это
позволяет держать весь инструментарий сборки и языковой сервер внутри контейнера,
не проводя USB-пробник внутрь Docker.
```mermaid
flowchart LR
subgraph DC["Devcontainer (единственное место запуска)"]
C["tests/host/&lt;dir&gt;/test_&lt;name&gt;.c\nUnity [+ fff]"]
CP["CMakePresets.json\nhost-debug / host-release"]
JB["just/build.just\ntest-host"]
C --> CP --> JB
subgraph Host["Хост (macOS / Linux)"]
DS["just host::debug-server\npyocd gdbserver :3333"]
ML["MCU-Link (CMSIS-DAP)"]
DS --> ML
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
```
Host-тесты компилируются `clang-17` **на хосте** (не ARM GCC), исполняются
как обычные нативные бинарники под `ctest`. Никакого железа не требуется —
в отличие от HIL-тестов (см. [../hil/HIL_CREATE_TEST.md](../hil/HIL_CREATE_TEST.md)).
**Ключевой принцип:** `pyocd gdbserver` слушает на `0.0.0.0:3333`. Из контейнера
GDB подключается через `host.docker.internal:3333` — специальный DNS-алиас Docker,
резолвится в IP хост-машины.
---
## Шаг 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` |
### На хосте
Полное объяснение разницы и структуры — в
[tests/host/README.md §1](../../../tests/host/README.md#1-две-категории-тестируемых-модулей).
| Компонент | Роль | Источник |
| --------------------------------- | ------------------------------- | -------------------------- |
| `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
## Шаг 1 — Создать тестовый файл
| Компонент | Роль |
| -------------------------------------- | ---------------------------------------------- |
| `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
mkdir -p tests/host/<name>/
touch tests/host/<name>/test_<name>.c
GDB_PORT=3333
PYOCD_TARGET=mimxrt1050_quadspi
PYOCD_FREQUENCY=4000000
FCB_PATH=tools/host/dcd/w25q128_fdcb.bin
```
### Шаблон — категория A (без моков)
---
## Поддерживаемые прошивки
| Конфигурация 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
```bash
# 1. Хост — запустить GDB-сервер (оставить в отдельном терминале)
just host::debug-server
# 2. DevContainer — VSCode
# Run & Debug (Ctrl+Shift+D) → выбрать конфигурацию → F5
```
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 + код
```
Весь диапазон `0x600000000x6000FFFF` — один 64KB сектор: стирается и
записывается за одну транзакцию.
---
## RTT-логи
SEGGER RTT включён только в Debug-сборках (`SEGGER_RTT_ENABLED=ON`).
После старта отладки вкладка `TERMINAL → RTT` принимает вывод канала 0.
`cortex-debug` находит адрес буфера по символу `_SEGGER_RTT` из ELF.
```c
#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)
#include "SEGGER_RTT.h"
SEGGER_RTT_printf(0, "value = %d\n", value);
```
---
## Шаг 3 — Собрать и прогнать
## 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-символов.
---
## Быстрый старт (первый запуск)
```bash
# конфигурация (один раз или после изменения CMakeLists)
cmake --preset host-debug
# 1. Убедиться что в .devcontainer/devcontainer.json есть (для Linux):
# "runArgs": ["--add-host=host.docker.internal:host-gateway"]
# сборка + тесты одной командой
just build::test-host
# 2. Залить прошивку
just host::flash-test-debug
# конкретный тест с полным выводом Unity
ctest --preset host-debug-test -R test_<name> -V
# 3. Хост — запустить GDB-сервер
just host::debug-server
# напрямую — без обёртки CTest
./build/host-debug/tests/host/test_<name>
# 4. DevContainer — VSCode
# Ctrl+Shift+D → 🐛 Debug: firmware_test → F5
```
`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 — зелёная сборка + прогон
.
├── .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).

View file

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

View file

@ -1,168 +0,0 @@
# bootloader
Загрузчик MIMXRT1052: выбирает и запускает приложение `tft_app` из одного из двух слотов
(MCUboot, Direct-XIP), обновляет его с microSD, восстанавливает плату при зависании образа. Сам
загрузчик прошивается только по USB ROM (blhost) или SWD — в поле не обновляется. Канал диагностики —
USB CDC ACM (JSON-строки).
**Принцип работы** (карта памяти, выбор образа, обновление, версии, даунгрейд, recovery) —
[BOOT_FLOW.md](../../docs/bootloader/BOOT_FLOW.md). LED-индикация — [LED_PATTERNS.md](../../docs/bootloader/LED_PATTERNS.md).
---
## Быстрый старт
### Сборка
```bash
just build::build-bootloader-debug # bootloader.elf/.bin
just build::hab-bootloader-debug # HAB-контейнер bootloader_hab.bin
```
### Прошивка
SWD (для итеративной разработки, не требует смены boot-режима платы):
```bash
just host::flash-swd-bootloader-debug
# после прошивки обязателен power cycle платы
```
USB ROM (SDP, плата в режиме Serial Downloader):
```bash
just host::flash bootloader debug
```
### Подключение
```bash
screen /dev/cu.usbmodemXXXX # macOS; порт свой на каждое подключение
```
```json
→ {"type":"cmd","cmd":"ping"}
← {"type":"pong"}
→ {"type":"cmd","cmd":"get_version"}
← {"type":"version_response","fw":"0.1.0"}
```
USB поднимается на каждой загрузке до обращения к SD, поэтому статусы видны, даже если подключиться
заранее.
### Отладка
VSCode → `🐛 Debug: bootloader` — пересобирает, подключается к GDB-серверу
(`just host::debug-server` должен быть запущен), останавливается на `main`. Под отладчиком аппаратный
watchdog приостановлен, пошаговая отладка сбросами не сбивается.
---
## Архитектура
Загрузчик **не зависит от SDRAM** для своей работы (XIP только из W25Q, без DCD) и без дисплея/RTOS:
инициализация, доступ к QSPI-flash, чтение FatFS с SD, проверка и выбор образа, прыжок. SEMC/SDRAM
трогаются только диагностически (`bsp_sdram_configure()`, boot-time smoke-test) — реально их поднимает
для себя уже само приложение в своём раннем startup.
Линкер жёстко ограничивает код бюджетом области загрузчика (256 КБ) с `ASSERT` на границу Slot A —
превышение становится ошибкой сборки, а не тихим заездом в чужую область.
```text
firmware/bootloader/
├── src/
│ ├── main.c — точка входа: инициализация → одна попытка загрузки
│ │ (SD-скан + recovery-гейт + прыжок) → цикл ожидания
│ ├── boot_select.* — выбор валидного слота и прыжок в выбранный образ
│ ├── slot_version.* — read-only проверка и чтение версии слота (без побочных
│ │ эффектов на flash)
│ ├── update_policy.* — чистая логика «ставить/пропустить» + целевой слот
│ ├── recovery.* — чистая логика решения recovery (порог / фолбэк / режим)
│ ├── sd_update.* — оркестрация: смонтировать SD, найти TFT_APP.BIN,
│ │ установить в целевой слот с потоковой verify-записью
│ ├── cli.* — построчный IO + диспетчеризация команд
│ ├── protocol.* — сериализация исходящих событий
│ ├── led_status.* — словарь LED-паттернов (см. ../../docs/bootloader/LED_PATTERNS.md)
│ ├── dev_sdram_test.* — [DEV-ONLY, Debug] глубокий тест SDRAM по команде "sdram_test"
│ └── version.h.in — шаблон версии (CMake → generated/version.h)
├── mcuboot_port/ — интеграция bootutil (MCUboot) поверх bsp_qspi_flash:
│ flash_area_* на 2 слота, конфиг, публичный ключ ECDSA-P256
├── fatfs/ — read-only FatFS для чтения TFT_APP.BIN с карты
└── test_stub/ — самостоятельный подписанный образ-заглушка вместо
tft_app для аппаратной проверки загрузчика
```
`update_policy` и `recovery` — чистые функции без доступа к железу, целиком покрыты host-тестами.
`boot_select`, `sd_update`, `flash_map_backend` — тонкий аппаратный слой поверх них.
---
## Протокол
USB CDC ACM, JSON-строки. Входящая строка — до `CLI_LINE_BUF_SIZE` (128) байт, исходящее сообщение —
до `PROTO_BUF_SIZE` (192) байт (несимметрично: `qspi_info`/`sdram_test` длиннее старых сообщений).
**Команды хоста:**
| Команда | Ответ |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `{"type":"cmd","cmd":"ping"}` | `{"type":"pong"}` |
| `{"type":"cmd","cmd":"get_version"}` | `{"type":"version_response","fw":"X.Y.Z"}` |
| `{"type":"cmd","cmd":"wdog"}` | `{"type":"wdog","armed":…,"timeout_s":…,"recovered":…,"reset_count":…,"threshold":…}` |
| `{"type":"cmd","cmd":"smoke_status"}` | `{"type":"status","state":"smoke_pass"}` / `"smoke_fail"` — результат boot-time smoke-теста SDRAM/SEMC (переспрос, см. ниже) |
| `{"type":"cmd","cmd":"qspi_info"}` | `{"type":"qspi_info","chip":"W25Q128","mfr":"0xEF","cap_byte":"0x18","size_mb":16,"pass":true}` (переспрос, см. ниже) |
| `{"type":"cmd","cmd":"sdram_test"}` <br> **[DEV-ONLY, Debug-сборка]** | серия из 6 `{"type":"sdram_test","phase":"…","pass":…,"duration_ms":…,"fail_addr":"…","expected":"…","got":"…"}` (`configure`/`address_bus`/`data_bus`/`sequential`/`retention`/`summary`) — блокирует главный цикл на ~4 с. Нет в Release/HAB (`BOOTLOADER_DEV_DIAGNOSTICS`) |
`smoke_status`/`qspi_info` ничего не отвечают, если соответствующий boot-time чек ещё не отработал —
в штатной последовательности `main.c` такого не бывает.
**Исходящие статусы** `{"type":"status","state":"…"}`:
| Состояние | Когда |
| ---------------- | ------------------------------------------ |
| `waiting_for_sd` | нет валидного слота, ждём карту |
| `installing` | идёт запись образа в слот |
| `update_skipped` | кандидат отклонён по версии |
| `recovery_mode` | плата в режиме восстановления |
| `smoke_pass` | boot-time smoke-тест SDRAM/SEMC прошёл |
| `smoke_fail` | boot-time smoke-тест SDRAM/SEMC провалился |
**Ошибки** `{"ok":false,"error":"…"}`: `SD_CANDIDATE_INVALID`, `SD_INSTALL_WRITE_FAILED`,
`SD_INSTALL_REJECTED`, `SD_DOWNGRADE_ERASE_FAILED`, `PARSE_ERR`, `UNKNOWN_CMD`, `LINE_TOO_LONG`.
**Автоматически на старте, без команды:** `wdog` — если предыдущий сброс был по watchdog
(`recovered:true`); `qspi_info` и `smoke_pass`/`smoke_fail` — сразу после соответствующей проверки.
Все три — best-effort: хост почти никогда не успевает открыть порт к этому моменту (USB enumeration),
поэтому у `qspi_info`/`smoke_status` (но не у одноразового boot-time `wdog`) есть команда-переспрос
в таблице выше.
LED-индикация, соответствующая этим состояниям, — [LED_PATTERNS.md](../../docs/bootloader/LED_PATTERNS.md).
---
## Тесты
**Host** — вся логика без железа (выбор слота, сравнение версий, политика установки, решение
recovery, протокол, CLI) на in-memory flash-фейках:
```bash
just build::test-host
```
**Аппаратный стенд** — сборка и подпись образов-заглушек, замещающих `tft_app` при ручной проверке
на плате (здоровые образы + варианты с зависанием на разных стадиях для проверки восстановления):
```bash
just build::build-mcuboot-stub
```
Чек-листы ручной проверки лежат рядом со стендом в `test_stub/`.
---
## Версионирование
Версия задаётся `project(bootloader VERSION X.Y.Z)` в `CMakeLists.txt` и прокидывается через
`configure_file(src/version.h.in → generated/version.h)` в строку, которую возвращает
`get_version`. `version.h` генерируется, вручную не редактируется.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,76 +0,0 @@
/**
* @file led_status.h
* @brief Индикация bootloader на двух LED единый словарь паттернов.
*
* Полное человекочитаемое описание (для сервисных инженеров)
* docs/bootloader/LED_PATTERNS.md. Здесь программный контракт; периоды
* в мс заданы в led_status.c и обязаны совпадать с тем документом.
*
* Разделение на два вида вызова не случайно:
* - Фоновые (устойчивые) состояния рисует главный цикл main.c каждую
* итерацию: led_status_draw_background().
* - Паттерн «идёт установка» рисуется ИЗНУТРИ блокирующей установки (главный
* цикл в это время не исполняется): led_status_tick_install(), обёрнутый в
* окно led_status_install_begin()/_end(). Вне окна tick no-op, поэтому
* его безопасно звать из flash_area_erase(), который дёргается и на
* revert/recovery-стирании, а не только на установке.
*/
#ifndef LED_STATUS_H_
#define LED_STATUS_H_
/** @brief Фоновое (устойчивое) состояние индикации. */
typedef enum
{
LED_BG_WAITING, /**< Норма: HEARTBEAT 50/450, APP выкл — ждём microSD. */
LED_BG_HW_FAULT, /**< Неисправность: HEARTBEAT 50/450, APP 100/100. */
LED_BG_RECOVERY, /**< Recovery (Фаза 6): оба LED синхронно 100/100. */
} led_bg_t;
/**
* @brief Нарисовать фоновый паттерн по текущему тику.
*
* Вызывать каждую итерацию главного цикла. Приоритет разрешается на стороне
* вызывателя (recovery > неисправность > норма) сюда приходит уже
* выбранное состояние.
*/
void led_status_draw_background(led_bg_t bg);
/** @brief Открыть окно «идёт установка» — с этого момента tick рисует паттерн. */
void led_status_install_begin(void);
/** @brief Закрыть окно «идёт установка» — tick снова становится no-op. */
void led_status_install_end(void);
/**
* @brief Обновить APP/HEARTBEAT под паттерн установки (HEARTBEAT 50/450,
* APP 250/250). No-op вне окна install.
*
* Звать из ВСЕХ блокирующих циклов установки и поблочного стирания слота
* (flash_area_erase, ~5 c на 2 МБ), и копирования чанков (sd_update).
*
* @note [ИЗВЕСТНОЕ ОГРАНИЧЕНИЕ, подтверждено на железе] Во время самого
* стирания блока (~150 мс на 64 КБ, bsp_qspi_erase_block_64k())
* qspi_irq_lock() держит __disable_irq() на всю длительность busy-wait
* это глушит и SysTick, на котором держится bsp_tick_get_ms()
* (см. bsp/tick/src/tick.c). Часы, от которых считается фаза мигания,
* не идут внутри каждого такого окна тик вызывается исправно между
* блоками, но «сейчас» между вызовами почти не меняется, поэтому
* глазом видно подвисание/дёрганое мигание, а не плавные 250/250.
* Во время копирования чанков (страницы по ~3 мс) окна намного короче
* там мигание заметно более гладкое. Осознанно не чиним (означало бы
* не маскировать IRQ на время IP-команды прямой путь к HardFault,
* см. bsp/qspi_flash/README.md, «XIP-безопасность»); фиксируем как
* факт в HARDWARE_VERIFICATION_LED_PATTERNS.md.
*/
void led_status_tick_install(void);
/**
* @brief Разовая индикация «образ с SD отклонён»: APP мигает 4×(80/80).
*
* Блокирующая (~640 мс), кормит watchdog по ходу. Оставляет APP выключенным
* фоновый паттерн восстановит главный цикл на следующей итерации.
*/
void led_status_flash_image_rejected(void);
#endif /* LED_STATUS_H_ */

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,186 +0,0 @@
# firmware/bootloader/test_stub/CMakeLists.txt
#
# Заглушка tft_app для аппаратной верификации корректной работы загрузчика
#
# Один main.c, много таргетов: адрес слота (--defsym __slot_base__) + частота
# мигания (STUB_BLINK_MS) — чтобы на глаз отличить, какой слот выбрал bootloader
# плюс три независимые оси confirm/hang/watchdog — чтобы прогнать классы
# отказов A/B на реальном железе. Подписывается вручную imgtool.
#
include(
${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/bootutil_sources.cmake)
function(add_mcuboot_stub)
cmake_parse_arguments(
ARG ""
"NAME;SLOT_BASE;BLINK_MS;OWN_SLOT_ID;CONFIRM_MODE;HANG_MODE;FEED_WDOG" ""
${ARGN})
if(NOT DEFINED ARG_CONFIRM_MODE)
set(ARG_CONFIRM_MODE 0) # 0 сразу / 1 отложенно / 2 никогда — см. main.c
endif()
if(NOT DEFINED ARG_HANG_MODE)
set(ARG_HANG_MODE 0) # 0 никогда / 1 до health-mark / 2 после health-mark /
# 3 после confirm
endif()
if(NOT DEFINED ARG_FEED_WDOG)
set(ARG_FEED_WDOG 1) # 1 кормить (дефолт, как в проде) / 0 не кормить
endif()
add_executable(
${ARG_NAME}
main.c
${BSP_GENERATED}/clock_config.c
${BSP_STARTUP_FILE}
${BSP_SYSCALLS_FILE}
${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/flash_map_backend.c
${CMAKE_SOURCE_DIR}/firmware/bootloader/src/led_status.c
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c)
# led_status.h — flash_map_backend.c (общий с bootloader) зовёт
# led_status_tick_install() из поблочного erase (см. led_status.h) — не
# добавляет стабу нового поведения (окно установки здесь никогда не
# открыто, tick — no-op), просто нужен для компиляции общего файла.
target_include_directories(
${ARG_NAME} PRIVATE ${MCUBOOT_BOOTUTIL_INCLUDES}
${CMAKE_SOURCE_DIR}/firmware/bootloader/src)
target_compile_definitions(
${ARG_NAME}
PRIVATE STUB_BLINK_MS=${ARG_BLINK_MS}
STUB_OWN_SLOT_ID=${ARG_OWN_SLOT_ID}
STUB_CONFIRM_MODE=${ARG_CONFIRM_MODE}
STUB_HANG_MODE=${ARG_HANG_MODE}
STUB_FEED_WDOG=${ARG_FEED_WDOG}
__STARTUP_CLEAR_BSS)
# Вендоренный bootutil_public.c — не наш стиль/warnings, тот же обход, что и
# для остального bootutil (см. bootutil_sources.cmake). Наш код (main.c,
# flash_map_backend.c) остаётся под обычными warnings.
set_source_files_properties(
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c
PROPERTIES COMPILE_OPTIONS "${MCUBOOT_VENDORED_COMPILE_OPTIONS}")
# bsp_wdog: загрузчик взводит WDOG перед прыжком, WDE — write-once, поэтому
# образ ОБЯЗАН его кормить, если STUB_FEED_WDOG=1 (иначе reset-loop — либо
# намеренно, для теста самого механизма). bsp_boot_state —
# bsp_boot_health_mark(). bsp_qspi_flash — нужен flash_map_backend.c для
# confirm_self().
target_link_libraries(
${ARG_NAME}
PRIVATE bsp_board
bsp_led
bsp_tick
bsp_boot_xip_no_dcd
bsp_wdog
bsp_boot_state
bsp_qspi_flash)
target_link_options(
${ARG_NAME}
PRIVATE
-Wl,--gc-sections
-Wl,--print-memory-usage
-Wl,-Map=${CMAKE_BINARY_DIR}/${ARG_NAME}.map
-Wl,--defsym=__slot_base__=${ARG_SLOT_BASE}
-Wl,--defsym=__stack_size__=0x400
-Wl,--defsym=__heap_size__=0x400
-T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld)
set_target_properties(${ARG_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR})
add_custom_command(
TARGET ${ARG_NAME}
POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${ARG_NAME}>
${CMAKE_BINARY_DIR}/${ARG_NAME}.bin
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${ARG_NAME}>
COMMENT "Generating ${ARG_NAME}.bin")
endfunction()
# ─────────────────────────────────────────────────────────────────────────
# Базовые "здоровые" заглушки: подтверждаются сразу, никогда не виснут. Slot A
# мигает раз в 500 мс ("версия 1"), Slot Б — раз в 250 мс ("версия 2").
# CONFIRM_MODE/HANG_MODE/FEED_WDOG — дефолты (0/0/1).
# ─────────────────────────────────────────────────────────────────────────
add_mcuboot_stub(
NAME
test_slot_stub_a
SLOT_BASE
0x60040000
BLINK_MS
500
OWN_SLOT_ID
0)
add_mcuboot_stub(
NAME
test_slot_stub_b
SLOT_BASE
0x60240000
BLINK_MS
250
OWN_SLOT_ID
1)
# Класс A: никогда не подтверждается, виснет СРАЗУ (до health-mark) — свежий
# неподтверждённый образ виснет до всякого прогресса. WDOG-сброс → штатный
# MCUboot revert (copy_done без image_ok) → откат, без участия recovery.c.
add_mcuboot_stub(
NAME
test_slot_stub_a_hang
SLOT_BASE
0x60040000
BLINK_MS
500
OWN_SLOT_ID
0
CONFIRM_MODE
2
HANG_MODE
1)
add_mcuboot_stub(
NAME
test_slot_stub_b_hang
SLOT_BASE
0x60240000
BLINK_MS
250
OWN_SLOT_ID
1
CONFIRM_MODE
2
HANG_MODE
1)
# Класс B: подтверждается СРАЗУ (успевает помигать наблюдаемо — видно, что образ
# живой и подтверждён), виснет через STUB_CONFIRM_DELAY_MS + STUB_HANG_DELAY_MS
# (~6 c) после старта — уже ПОСЛЕ confirm. Штатный revert MCUboot тут не
# сработает (image_ok уже SET) — ловит именно recovery.c (счётчик
# watchdog-сбросов + фолбэк/recovery).
add_mcuboot_stub(
NAME
test_slot_stub_a_confirm_hang
SLOT_BASE
0x60040000
BLINK_MS
500
OWN_SLOT_ID
0
CONFIRM_MODE
0
HANG_MODE
3)
add_mcuboot_stub(
NAME
test_slot_stub_b_confirm_hang
SLOT_BASE
0x60240000
BLINK_MS
250
OWN_SLOT_ID
1
CONFIRM_MODE
0
HANG_MODE
3)

View file

@ -1,161 +0,0 @@
/**
* @file main.c
* @brief Заглушка tft_app для аппаратной верификации bootutil (Фаза 2) и
* recovery-логики (Фаза 6).
*
* tft_app ещё не реализована bootloader'у некуда прыгать. Этот образ
* минимальный, но настоящий imgtool-подписанный XIP-образ с корректным
* vector table по адресу слота: базово мигает LED_APP с периодом,
* зависящим от STUB_BLINK_MS (задаётся компилятору), чтобы по частоте
* мигания визуально отличить, какой слот реально выбрал bootloader. См.
* firmware/bootloader/PLAN.md, "Аппаратная верификация Фазы 2".
*
* Фаза 6 добавила три независимые, настраиваемые компилятором оси
* стенд-контракт tft_app (см. PLAN.md, 6c) минимально, но по-настоящему:
* - STUB_CONFIRM_MODE 0 сразу / 1 отложенно (STUB_CONFIRM_DELAY_MS) /
* 2 никогда. Подтверждение boot_set_next(fap, true, true) на
* СОБСТВЕННОМ слоте (STUB_OWN_SLOT_ID), НЕ boot_set_confirmed() та
* жёстко пишет в FLASH_AREA_IMAGE_PRIMARY (Slot A) независимо от того,
* откуда реально исполняется код: для стаба в Slot Б это подтвердило бы
* чужой слот, а не себя.
* - STUB_HANG_MODE 0 никогда / 1 до health-mark (сразу на входе) /
* 2 после health-mark, до confirm / 3 после confirm (через
* STUB_HANG_DELAY_MS после факта подтверждения чтобы успеть увидеть
* мигание глазами перед тем как оно застынет).
* - STUB_FEED_WDOG 1 (дефолт, как в проде) / 0 не кормить watchdog,
* детерминированно проверить сам механизм сброса.
*
* Без USB/CDC визуальной индикации (частота/застывание LED_APP) достаточно
* для чек-листа Фазы 6, минимальный код.
*
* Watchdog: загрузчик взводит аппаратный WDOG перед прыжком сюда, а WDE
* write-once (выключить нельзя). Поэтому заглушка, если сконфигурирована
* его кормить (STUB_FEED_WDOG=1, дефолт), обязана делать это в главном цикле
* иначе WDOG сбросит плату через таймаут (см. bsp/wdog/README.md).
*/
#include "board.h"
#include "bootutil/bootutil_public.h"
#include "bsp/boot_state.h"
#include "bsp/led.h"
#include "bsp/tick.h"
#include "bsp/wdog.h"
#include "flash_map.h"
#include <stdbool.h>
#ifndef STUB_BLINK_MS
#error "STUB_BLINK_MS must be defined (see firmware/bootloader/test_stub/CMakeLists.txt)"
#endif
#ifndef STUB_OWN_SLOT_ID
#error "STUB_OWN_SLOT_ID must be defined (0 = Slot A, 1 = Slot Б)"
#endif
/* Confirm mode: 0 = сразу, 1 = отложенно, 2 = никогда. */
#ifndef STUB_CONFIRM_MODE
#define STUB_CONFIRM_MODE 0
#endif
#ifndef STUB_CONFIRM_DELAY_MS
#define STUB_CONFIRM_DELAY_MS 3000U
#endif
/* Hang mode: 0 = никогда, 1 = до health-mark, 2 = после health-mark (до
* confirm), 3 = после confirm. */
#ifndef STUB_HANG_MODE
#define STUB_HANG_MODE 0
#endif
#ifndef STUB_HANG_DELAY_MS
#define STUB_HANG_DELAY_MS 3000U
#endif
#ifndef STUB_FEED_WDOG
#define STUB_FEED_WDOG 1
#endif
/**
* @brief Подтвердить СОБСТВЕННЫЙ слот (STUB_OWN_SLOT_ID).
*
* boot_set_next(fap, active=true, confirm=true) не boot_set_confirmed():
* та жёстко работает с FLASH_AREA_IMAGE_PRIMARY (Slot A) вне зависимости от
* того, какой слот реально исполняется; для Direct-XIP с двумя равноправными
* слотами это подтвердило бы не тот слот при исполнении из Slot Б.
*/
static void confirm_self(void)
{
const struct flash_area *p_fap;
if (flash_area_open((uint8_t) STUB_OWN_SLOT_ID, &p_fap) == 0)
{
(void) boot_set_next(p_fap, true, true);
flash_area_close(p_fap);
}
}
int main(void)
{
board_hw_init();
bsp_led_init();
bsp_tick_init();
#if STUB_HANG_MODE == 1
for (;;) { } /* до health-mark — Класс A: свежий образ виснет сразу */
#endif
/* health-mark == bsp_boot_attempt_reset() (см. bsp/boot_state.h) — вызывать
* его безусловно перед потенциальным зависанием НЕЛЬЗЯ: он обнулял бы
* счётчик попыток на КАЖДОМ цикле ДО того, как зависание успевает
* засчитаться, и recovery-фолбэк/recovery-режим не сработали бы никогда
* (найдено на железе HANG_MODE=3 резетился бесконечно вместо остановки
* на пороге). Это тот же класс "честной границы", что уже описан в
* PLAN.md (образ обнуляет счётчик, ПОТОМ виснет таймером не отличить
* "здоров" от "здоров, но детерминированно виснет"): здесь HANG_MODE
* снимает эту неоднозначность на этапе компиляции если конфигурация
* гарантированно виснет (2 после health-mark, до confirm; 3 после
* confirm), health-mark не зовём вообще, счётчик копится корректно.
* Здоровый стаб (HANG_MODE 0) и "виснет до health-mark" (1, сюда и не
* доходит) не затронуты. */
#if (STUB_HANG_MODE != 2) && (STUB_HANG_MODE != 3)
bsp_boot_health_mark(); /* "дошёл до устойчивого состояния" (Фаза 6, 6c) */
#endif
#if STUB_HANG_MODE == 2
for (;;) { } /* после (несостоявшегося) health-mark, до confirm */
#endif
bool confirmed = false;
#if STUB_CONFIRM_MODE == 0
confirm_self();
confirmed = true;
#endif
uint32_t start_ms = bsp_tick_get_ms();
(void) start_ms; /* не используется, если ни один из режимов ниже её не читает */
while (1)
{
#if STUB_FEED_WDOG
bsp_wdog_refresh(); /* обслуживаем унаследованный от загрузчика WDOG */
#endif
#if STUB_CONFIRM_MODE == 1
if (!confirmed && ((bsp_tick_get_ms() - start_ms) >= STUB_CONFIRM_DELAY_MS))
{
confirm_self();
confirmed = true;
}
#endif
#if STUB_HANG_MODE == 3
if (confirmed &&
((bsp_tick_get_ms() - start_ms) >= (STUB_CONFIRM_DELAY_MS + STUB_HANG_DELAY_MS)))
{
for (;;) { } /* после confirm — Класс B: подтверждённый образ виснет в рантайме */
}
#endif
bsp_led_toggle(LED_APP);
bsp_delay(STUB_BLINK_MS);
}
}

View file

@ -3,7 +3,7 @@
cmake_minimum_required(VERSION 3.20)
project(
firmware_test
VERSION 0.1.2
VERSION 0.1.1
LANGUAGES C ASM)
set(TARGET_NAME firmware_test)

488
firmware/test/PLAN.md Normal file
View file

@ -0,0 +1,488 @@
# 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`, избыточно
---
## Закрытые архитектурные решения
> Не пересматривать без явного запроса.
### Этапы 15 (ранее зафиксированные)
- **Транспорт:** 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)
```

View file

@ -1,12 +1,10 @@
# firmware_test
> Диагностическая прошивка для плат TFT индикаторов, вернувшихся по
> рекламации. Загружается на таргет сервисным инженером через USB (через BootROM IMXRT1052).
> Диагностическая прошивка для плат **MIMXRT1052CVJ5B**, вернувшихся по
> рекламации. Запускается сервисным инженером через USB CDC ACM без
> предварительной прошивки загрузчика.
>
>Версия прошивки: `0.1.2` | Протокол: v2
>
>Версия — из `project(firmware_test VERSION X.Y.Z)` в `CMakeLists.txt`
> (см. [Версионирование](#версионирование))
> Версия прошивки: `0.1.0` | Протокол: v2
---
@ -47,7 +45,7 @@ just build::hab-firmware-test-debug
# Перевести плату в SDP-режим: BOOT_MOD_1 → 3V3 → Reset
just host::flash-test-debug
# Или через SWD
# Или через SWD (power cycle после)
just host::flash-swd-test-debug
```
@ -68,7 +66,7 @@ screen /dev/ttyACM0
После подключения таргет сразу присылает:
```json
{"type":"session_start","fw":"0.1.2","target":"IMXRT1052","uptime_ms":0}
{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
```
Проверка связи:
@ -83,7 +81,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":15304,"detail":""}
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
```
Запуск всех тестов:
@ -112,12 +110,12 @@ just build::test-host
│ JSON-lines, 1 строка = 1 сообщение
[Плата MIMXRT1052 с firmware_test]
│ GPIO / LPUART / SEMC / FlexSPI / USDHC / SAI(MQS)
│ GPIO / LPUART / SEMC / FlexSPI / USDHC
[Периферия: SDRAM, QSPI Flash, uSD, Display, Кнопки, CAN, MQS, Opto]
[Периферия: SDRAM, QSPI Flash, uSD, Display, Кнопки, CAN, UART, Opto]
[M5StampPLC — управление внешними сигналами для HIL тестов]
(реле → EXT_IN1/IN2, RS_RX; CAN loopback)
(реле → EXT_IN1/IN2, RS_RX, CAN, UART echo)
```
**Принцип разделения ответственности:**
@ -125,8 +123,8 @@ just build::test-host
- Вся тест-логика живёт **на таргете** (`test_runner.c`, `tests/*.c`).
- Хост — тонкий клиент: отправляет команды, отображает события, управляет
интерактивными шагами через `confirm`.
- Тесты **атомарны**: инженер запускает один тест, подмножество или все сразу —
порядок не фиксирован.
- Тесты **атомарны**: инженер запускает один тест или все сразу — порядок
не фиксирован.
---
@ -138,8 +136,6 @@ firmware/test/
└── src/
├── main.c — инициализация BSP, главный цикл
├── version.h.in — шаблон версии (CMake → generated/version.h)
├── cli.h / cli.c — IO-слой
│ буферизация строк, парсинг "type",
│ диспатч на test_runner / protocol
@ -153,23 +149,20 @@ firmware/test/
├── test_runner.h / .c — реестр + state machine
│ IDLE → PRE_CONFIRM → RUNNING → IDLE
│ test_runner_wait_confirm() для in-run confirm
│ test_runner_wait_confirm() для display
└── tests/
├── test_sdram.c — SDRAM 32 MB (self)
├── 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_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_can.c — CAN loopback (HIL)
└── test_mqs.c — MQS Audio Out (interactive, in-run confirm)
├── 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)
```
> Порядок файлов в `tests/` — как в `CMakeLists.txt`. Порядок **выполнения**
> тестов определяется реестром `k_registry[]` в `test_runner.c`
> (см. [Матрица тестов](#матрица-тестов)).
---
### Граф зависимостей
@ -182,9 +175,8 @@ main.c
├── bsp_usb_cdc (USB CDC ACM, единственный транспорт)
├── cli.c
│ └── bsp_usb_cdc (read / write)
│ └── 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 (send_error, send_pong)
│ └── test_runner.c (run_single, run_all, on_confirm)
├── protocol.c
│ └── cli.c (cli_send)
│ └── bsp_tick (bsp_tick_get_ms — для uptime)
@ -196,28 +188,26 @@ main.c
└── tests/*.c (тест-модули через реестр)
```
**BSP-зависимости тест-модулей** (по `target_link_libraries` в `CMakeLists.txt`):
**BSP-зависимости тест-модулей:**
| Тест | 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`).
| `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` ✅ |
---
### State machine test_runner
```bash
cmd: run / run_all / run_selected
cmd: run / run_all
┌─────────────────────────────────────┐
@ -243,8 +233,8 @@ main.c
timeout → SKIP │ │
│ │
run: IDLE ──┘ │
run_all / run_selected: следующий тест ───┘
done: protocol_send_summary()
run_all: следующий тест в реестре ───┘
run_all done: protocol_send_summary()
```
**Ключевые свойства state machine:**
@ -252,12 +242,10 @@ main.c
- `RUNNING` — защита от ложного `is_busy()==false` во время blocking `run()`.
Пока тест выполняется, новые команды получают `BUSY`.
- `test_runner_wait_confirm()` — вызывается из `run()` интерактивных тестов
(display, mqs). Внутри polling loop: `bsp_usb_cdc_poll()` + `cli_process()`.
(display). Внутри polling loop: `bsp_usb_cdc_poll()` + `cli_process()`.
USB-стек остаётся живым, confirm приходит без возврата в главный цикл.
- `run_selected` работает по той же машине над маской выбранных тестов
(`g_s_selected[]`); порядок — по реестру, не по порядку в запросе.
- `critical=true` + `FAIL` в `run_all`/`run_selected` → все оставшиеся тесты
получают `SKIP` немедленно, `summary.overall = "fail"`.
- `critical=true` + `FAIL` в `run_all` → все оставшиеся тесты получают
`SKIP` немедленно, `summary.overall = "fail"`.
---
@ -270,7 +258,7 @@ main.c
| Интерфейс | USB CDC ACM, разъём J2 |
| Кодировка | UTF-8 |
| Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` |
| Максимальная длина строки | 128 байт включая `\n` (`CLI_LINE_BUF_SIZE`) |
| Максимальная длина строки | 128 байт включая `\n` |
| CR+LF | Принимается (таргет отбрасывает `\r`) |
Нет хэндшейка, нет sequence number, нет подтверждений доставки.
@ -285,7 +273,7 @@ main.c
│ │
│ [USB SDP: прошивка загружена] │
│ [CDC ACM: порт открыт] │
│◄─── {"type":"session_start","fw":"0.1.2",...} │ автоматически
│◄─── {"type":"session_start","fw":"0.1.0",...} │ автоматически
│ │
│──── {"type":"cmd","cmd":"ping"} ─────────────►│
│◄─── {"type":"pong"} │
@ -302,8 +290,7 @@ main.c
```
`session_start` отправляется **автоматически** при каждом старте, до получения
первой команды. Хост должен быть готов принять его сразу после открытия порта
(либо не полагаться на него — фикстуры HIL проверяют живость через `ping`).
первой команды. Хост должен быть готов принять его сразу после открытия порта.
---
@ -323,7 +310,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":15304,"detail":""}
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
```
Если `id` не найден:
@ -337,58 +324,13 @@ main.c
```json
→ {"type":"cmd","cmd":"run_all"}
← {"type":"test_begin","id":"sdram",...}
← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""}
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
← {"type":"test_begin","id":"qspi",...}
← {"type":"test_result","id":"qspi","status":"pass","ms":86,"detail":""}
← {"type":"test_result","id":"qspi","status":"pass","ms":88,"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
@ -405,19 +347,35 @@ main.c
#### `session_start`
```json
{ "type":"session_start", "fw":"0.1.2", "target":"IMXRT1052", "uptime_ms":0 }
{
"type": "session_start",
"fw": "0.1.0",
"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":15304, "detail":"" }
{
"type": "test_result",
"id": "sdram",
"status": "pass",
"ms": 312,
"detail": ""
}
```
| `status` | Смысл |
@ -426,39 +384,37 @@ main.c
| `"fail"` | Тест провален; `detail` содержит описание (до 95 символов) |
| `"skip"` | Пропущен: нет оборудования, таймаут, отказ оператора, critical fail выше |
Примеры `detail`: `"addr=0x80200001 exp=0x02 got=0xFF"`, `"JEDEC: mfr=0xFF exp=0xEF"`.
#### `test_list`
Ответ на `list_tests` — массив дескрипторов (`id`, `name`, `critical`,
`requires_hil`).
Примеры `detail`: `"addr=0x80001000 expected=0xA5 got=0x00"`, `"JEDEC ID mismatch"`.
#### `confirm_request`
```json
{ "type":"confirm_request", "id":"display_red", "prompt":"Screen is solid red?", "timeout_ms":15000 }
{
"type": "confirm_request",
"id": "display_red",
"prompt": "Экран залит красным цветом?",
"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"}`.
---
### Ошибки протокола
@ -469,60 +425,43 @@ 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() вернул ошибку
```
---
### Интерактивные тесты
#### microSD — вставить карту (pre-confirm)
#### uSD — вставить карту
```bash
← {"type":"confirm_request","id":"usd","prompt":"Insert microSD card and press OK","timeout_ms":30000}
→ {"type":"confirm","id":"usd","confirmed":true}
← {"type":"confirm_request","id":"usd_insert","prompt":"Вставьте microSD","timeout_ms":30000}
→ {"type":"confirm","id":"usd_insert","confirmed":true}
← {"type":"test_begin","id":"usd",...}
← {"type":"test_result","id":"usd","status":"pass","ms":874,"detail":""}
← {"type":"test_result","id":"usd","status":"pass","ms":541,"detail":""}
```
confirm id для pre-confirm равен id теста (`usd`) — механизм `pre_confirm_prompt`
использует `mod->id`. Отказ или таймаут 30 с`SKIP`.
Если оператор отказался или таймаут:
#### Display RGB888 — подтвердить цвета и ротацию (in-run confirm)
```bash
← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"operator skipped"}
```
Шесть шагов: Red → Green → Blue → White, затем два ротационных
(диагностика непропаянных LR/UD пинов). Тест прерывается на **первом**
неподтверждённом шаге (короткое замыкание, не сбор всех ответов).
#### Display RGB888 — подтвердить цвета
Четыре шага R/G/B/W. Итог — AND всех подтверждений.
```bash
← {"type":"test_begin","id":"display",...}
← {"type":"confirm_request","id":"display_red","prompt":"Screen is solid red?","timeout_ms":15000}
← {"type":"confirm_request","id":"display_red","prompt":"Экран красный?","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":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",...}
→ {"type":"confirm","id":"display_white","confirmed":false}
← {"type":"test_result","id":"display","status":"fail","ms":22103,
"detail":"display_white not confirmed"}
```
#### Кнопки — нажать физически
@ -533,44 +472,36 @@ confirm id для pre-confirm равен id теста (`usd`) — механи
```bash
← {"type":"test_begin","id":"buttons",...}
← {"type":"confirm_request","id":"btn1_press","prompt":"Press Test_But_1","timeout_ms":10000}
← {"type":"confirm_request","id":"btn1_press","prompt":"Нажмите Test_But_1","timeout_ms":10000}
[таргет ждёт bsp_button — без JSON confirm от хоста]
← {"type":"confirm_request","id":"btn2_press","prompt":"Press Test_But_2","timeout_ms":10000}
← {"type":"test_result","id":"buttons","status":"pass",...}
← {"type":"confirm_request","id":"btn2_press","prompt":"Нажмите Test_But_2","timeout_ms":10000}
← {"type":"test_result","id":"buttons","status":"pass","ms":3821,"detail":""}
```
Таймаут 10 с`SKIP` (не FAIL).
> Полные потоки всех тестов (SDRAM/QSPI/opto/CAN, коды `detail`, HIL pytest) —
> в справочнике по тестированию `README_TESTING.md`.
---
## Матрица тестов
Порядок — как в реестре `k_registry[]` (`test_runner.c`).
| № | ID | Название | Тип | Critical | M5 HIL | Confirm |
| --- | --------- | ------------------ | ----------- | -------- | ------ | ------------- |
| 1 | `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
| 2 | `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ |
| 3 | `usd` | microSD (SDIO) | interactive | ❌ | ❌ | ✅ pre_confirm |
| 4 | `display` | TFT Display RGB888 | interactive | ❌ | ❌ | ✅ 6× в run() |
| 5 | `buttons` | Test Buttons | interactive | ❌ | ❌ | prompt only |
| 6 | `opto` | Opto Inputs | HIL | ❌ | ✅ | ✅ 6× (авто) |
| 7 | `can` | CAN loopback | HIL | ❌ | ✅ | ✅ 2× (авто) |
| 8 | `mqs` | MQS Audio Out | interactive | ❌ | ❌ | ✅ 1× в run() |
| ID | Название | Тип | Critical | M5 HIL | Confirm |
| ---------- | ---------------- | ---------------- | -------- | ------ | ------------- |
| `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 | ❌ | ✅ | ❌ |
**Типы confirm:**
- **pre_confirm**`test_runner` отправляет `confirm_request` до вызова `run()`,
ждёт JSON-ответ через state machine (асинхронно). id = id теста.
- **pre_confirm** — test_runner отправляет `confirm_request` до вызова `run()`,
ждёт JSON-ответ через state machine (асинхронно).
- **в 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`).
---
@ -609,7 +540,7 @@ static test_result_t test_foo_run(void)
return result;
}
const test_module_t K_TEST_FOO = {
const test_module_t k_test_foo = {
.id = "foo", /* короткий ASCII-ключ */
.name = "Foo Peripheral",
.critical = false, /* true → run_all стопится при fail */
@ -627,19 +558,15 @@ 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`
@ -651,6 +578,7 @@ add_executable(
src/cli.c
src/protocol.c
src/test_runner.c
src/tests/test_sdram.c
src/tests/test_foo.c # ← добавить
...
)
@ -664,18 +592,24 @@ target_link_libraries(
### Шаг 4 — Обновить матрицу тестов
Добавить строку в таблицу в этом README (и, если есть протокольный поток —
в `README_TESTING.md`).
Добавить строку в таблицу в этом README.
### Шаблоны для разных типов тестов
#### Self-тест с инициализацией
```c
static void test_foo_init(void) { bsp_foo_init(); }
static void test_foo_deinit(void) { bsp_foo_deinit(); }
static void test_foo_init(void)
{
bsp_foo_init();
}
const test_module_t K_TEST_FOO = {
static void test_foo_deinit(void)
{
bsp_foo_deinit();
}
const test_module_t k_test_foo = {
.id = "foo",
.init = test_foo_init,
.run = test_foo_run,
@ -688,6 +622,7 @@ 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)
{
@ -715,13 +650,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 (id = "foo") перед вызовом run() */
/* test_runner сам отправит confirm_request перед вызовом run() */
```
---
@ -792,34 +727,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` пока стек жив.
---
## Версионирование
Текущая версия ПО задается с помощью `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`:
`FIRMWARE_TEST_VERSION` в `protocol.h` — единственная точка правды о версии.
Поле `"fw"` в `session_start` несёт эту строку.
```
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"
```
При несовместимых изменениях протокола (новое обязательное поле, изменение
семантики) — bumping версии + обновление этого документа.
`version.h` генерируется, **не** редактируется вручную. Менять версию —
только в `CMakeLists.txt`.
Хост должен сверять `"fw"` при подключении и предупреждать оператора при
несовпадении ожидаемой версии.
Хост может запросить версию явно (`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