# test_buttons: finished, all documentation refactored

This commit is contained in:
Dmitry Akimov 2026-06-23 15:02:36 +03:00
parent 8d85ffe877
commit 293b61fff3
34 changed files with 2917 additions and 3945 deletions

1
.gitignore vendored
View file

@ -75,3 +75,4 @@ __pycache__/
tools/host/.venv-host/
tools/host/.venv-host-win/
.zed/
project_tree.txt

View file

@ -1,6 +1,6 @@
# Журнал изменений: `tft_manufacture_test`
Репозиторий: https://github.com/OSabuser/tft_manufacture_test.git
Репозиторий: <https://github.com/OSabuser/tft_manufacture_test.git>
Отслеживаемая ветка: `dev`
Базовый диапазон: 2026-03-05 .. 2026-05-08, 52 коммита
Базовый HEAD: `22f98c311a4564d8f18ed72d11635bf908046b1e`
@ -50,27 +50,67 @@
## [Не выпущено]
Диапазон: после `22f98c311a4564d8f18ed72d11635bf908046b1e`
### Кратко
- После базового среза новые изменения ещё не обрабатывались.
### Что отслеживать
- Проверять, остаётся ли `.github/workflows/ci.yml` только сборочным workflow через `just ci::build` или начинает запускать host-тесты.
- Отслеживать появление self-hosted runner, регулярных аппаратных прогонов или отчётов по HIL.
- Проверять новые firmware-test модули в `firmware/test/src/tests/` и синхронные обновления протокола/документации.
- Следить за развитием BSP: MQS, RGB, bootloader или `tft_app`.
- Отслеживать локальные патчи поверх vendor SDK, которые нужно вести отдельным patch log.
## [2026-06-23] — Buttons/display test modules и полный рефакторинг документации
Диапазон: `22f98c311a4564d8f18ed72d11635bf908046b1e..<NEW_SHA>`
Сравнение: <https://github.com/OSabuser/tft_manufacture_test/compare/22f98c311a4564d8f18ed72d11635bf908046b1e>...<NEW_SHA>
### Кратко
- Manufacturing-test firmware получил два новых test module (`test_buttons`, `test_display`), оба hardware-verified на таргете.
- Вся проектная документация прошла полный рефакторинг: единый шаблон для BSP README, Mermaid-диаграммы вместо ASCII, структурные README для `utils/` и `port/`.
### Добавлено
- `firmware/test/src/tests/test_buttons.c` — интерактивный тест двух тактовых кнопок (GPIO_B1_14/GPIO2[30], GPIO_B1_15/GPIO2[31]); non-blocking polling 5 мс, таймаут даёт `TEST_STATUS_SKIP`; `bsp_button_init()` вызывается внутри init-фазы модуля.
- `firmware/test/src/tests/test_display.c` — интерактивный тест дисплея (два этапа: заливка цветом R/G/B/W с confirm-запросами, проверка ротации LR/UD через `bsp_display_set_rotation()`).
- `port/fatfs/README.md` — новый документ, описывает INTERFACE-архитектуру `port_fatfs_sd` и причину per-binary компиляции `diskio_sd.c`.
- `utils/prio_queue/README.md` — новый документ (модуль существовал, документация отсутствовала).
### Изменено
- Все 11 BSP README (`led`, `tick`, `uart_host`, `opto`, `can`, `button`, `usb_cdc`, `sdram`, `qspi_flash`, `sd`, `display`) приведены к единому шаблону: Аппаратура (с номерами корпуса из pin_mux) → Архитектура → API → Быстрый старт → Тестирование → Интеграция → CMake.
- `bsp/README.md` — исправлена таблица компонентов (ранее отсутствовали 6 из 11 модулей), ASCII-диаграмма концепции заменена Mermaid.
- `docs/DEV_ARCH.md` — шесть ASCII-диаграмм заменены Mermaid (`graph TB`, `flowchart`, `sequenceDiagram`); §2 разбит на две отдельные диаграммы (физические связи + состав инструментов).
- `docs/HOW_TO_FLASH.md` — ASCII bash-поток USB SDP заменён Mermaid `flowchart TD`.
- `docs/HOW_TO_DEBUG.md` — ASCII топология хост/devcontainer заменена Mermaid `flowchart LR`.
- `docs/testing/PROTOCOL.md` — четыре ASCII-блока (стенд, жизненный цикл, три интерактивных теста, state machine) заменены Mermaid (`flowchart`, `sequenceDiagram`, `stateDiagram-v2`).
- `docs/testing/host/HOST_CREATE_TEST.md` — ASCII стек заменён Mermaid.
- `docs/testing/hil/HIL_CREATE_TEST.md` — ASCII стек и цепочка фикстур заменены Mermaid.
- `README.md` (корневой) — убрана устаревшая таблица BSP с 5 из 11 модулями, заменена ссылкой на `bsp/README.md`; исправлено описание `tft_app`.
- `utils/README.md` — добавлен пропущенный модуль `prio_queue`, добавлен раздел CMake.
- `port/README.md` — переработан с Mermaid-диаграммой архитектуры слоёв.
- `port/log/README.md` — добавлена Mermaid-диаграмма потока, раздел FreeRTOS с реальным кодом.
### Тесты
- `test_buttons` hardware-verified: обнаружен и исправлен баг `cli.c``g_s_line_len` должен сбрасываться в `0` до вызова `process_line()`, иначе входящие байты корруптят буфер команды в blocking confirm-wait цикле.
- `test_display` hardware-verified: уточнена семантика ротации — `SHLR` управляет горизонтальным направлением (LR пин), `UPDN` — вертикальным (UD пин); enum использует семантические имена (`BSP_DISPLAY_ROTATE_0`, `BSP_DISPLAY_FLIP_VERTICAL` и др.).
### Отфильтрованный шум
- Нормализация форматирования внутри уже существующих документов без изменения содержания.
- Перестановки в порядке разделов там, где смысл не менялся.
## Базовый срез — 2026-05-13
Диапазон: вся история репозитория от initial commit до `22f98c311a4564d8f18ed72d11635bf908046b1e`
### Кратко
- Репозиторий представляет собой firmware-монорепозиторий для платы на базе NXP MIMXRT1052CVJ5B. Он охватывает manufacturing-test firmware, планируемые bootloader/application firmware, BSP-модули, host tooling, HIL tooling и документацию.
- К базовому срезу в проекте уже есть полноценный BSP-слой, двухуровневая стратегия тестирования, USB-CDC архитектура тестовой прошивки, GitHub Actions CI и обширная инженерная документация.
### Текущая архитектура
- BSP-модули на момент базового среза: `led`, `tick`, `uart_host`, `opto`, `can`, `button`, `usb_cdc`, `sdram`, `qspi_flash`, `sd`, `display`.
- Host-тесты используют Unity/fff-подобные моки и инструменты, рассчитанные на devcontainer.
- HIL-тесты используют pyOCD, pyserial, pytest и поддержку M5StampPLC.
@ -78,98 +118,121 @@
- Документация покрывает архитектуру разработки, прошивку, отладку, HAB, HIL, host-тесты и поведение протокола.
### Состояние CI
- GitHub Actions уже есть и собирает проект внутри devcontainer.
- Видимый пробел: host unit tests и HIL tests пока не входят в GitHub Actions workflow.
## [2026-05-08] — Display test module и выравнивание документации
### Кратко
- Базовый срез заканчивается тем, что поддержка display становится частью и BSP, и manufacturing-test firmware.
- README и инженерные документы были синхронизированы между несколькими модулями, поэтому это documentation-heavy, но смысловое изменение состояния проекта.
### Добавлено
- Реализован `bsp/display` с API, исходниками и интеграцией в сборку.
- Добавлен `firmware/test/src/tests/test_display.c` как firmware-side test module для display.
### Изменено
- README нескольких BSP-модулей и manufacturing-test firmware приведены к более единому стилю.
- `docs/DEV_ARCH.md`, документы по прошивке/отладке и HIL-гайды обновлены под актуальную структуру проекта.
- `just/ci_workflow.md` расширен дополнительными деталями CI workflow.
### Документация
- Документация по display, SD, CAN, button, USB-CDC и firmware-test получила содержательные обновления, а не только форматирование.
### Удалено
- Удалён `bsp/qspi_flash/REFACTORING.md`, так как временный план потерял актуальность.
### Отфильтрованный шум
- Чистая нормализация стиля в документации не учитывалась как отдельная feature, если она не меняла содержание или проектные инструкции.
## [2026-05-07] — USD/SD test flow
### Кратко
- SD/MMC testing перешёл от BSP/middleware-работ к firmware-test module с обновлением протокола и тестовой документации.
### Добавлено
- Добавлен `firmware/test/src/tests/test_usd.c`, затем доведён до usable SD/MMC test module.
### Изменено
- `firmware/test/src/main.c` упрощён по мере модульного оформления test modules.
- `docs/testing/PROTOCOL.md`, `firmware/test/src/tests/README.md` и `firmware/test/PLAN.md` обновлены под USD/SD test coverage.
### Тесты
- Firmware-side SD/MMC testing стал видимым отдельным модулем, хотя базовый срез всё ещё требует отслеживать стабильное HIL-покрытие SD.
## [2026-05-06] — SD card и FatFS integration
### Кратко
- Поддержка SD card и FatFS вошла в BSP и firmware-test stack.
- NXP SD middleware был пропатчен, что создаёт будущую точку контроля для vendor SDK drift.
### Добавлено
- Добавлен `bsp/sd` с API, реализацией, README и CMake-интеграцией.
- Добавлены `firmware/test/fatfs` и `port/fatfs/sd` для подключения FatFS к SD BSP.
- Добавлены generated SDMMC configuration files.
### Изменено
- SDK SD middleware и SDMMC host code изменены для поддержки smoke-test path.
- Clock и pin configuration скорректированы под SD/MMC.
### Тесты
- Появился SD smoke-test, но стабильное HIL regression coverage ещё не закреплено в базовом срезе.
### Отфильтрованный шум
- `project_tree.txt` и `sdk/sdk_tree.txt` рассматривались как artifacts состояния репозитория, а не как функциональные изменения.
## [2026-04-23] — Добавлен GitHub Actions CI
### Кратко
- CI стал реальным: проект получил GitHub Actions workflow, который собирает проект внутри devcontainer.
- Workflow ориентирован на сборку; host tests и HIL tests остаются будущей работой.
### Добавлено
- Добавлен `.github/workflows/ci.yml` с triggers на push, PR и manual dispatch.
- Workflow собирает devcontainer image, запускает `just ci::build` и загружает build tree как artifact.
- Добавлен `just/ci_workflow.md` с описанием CI flow.
### Изменено
- `just/ci.just` скорректирован под CI build path.
### CI
- CI покрывает воспроизводимость containerized build.
- CI пока не запускает host unit tests.
- CI пока не запускает HIL tests, которым требуется подключённое железо или self-hosted runner.
### Удалено
- Удалён `.clang-tidy` override для generated code из `bsp/generated`.
## [2026-04-20 .. 2026-04-22] — SDRAM и QSPI firmware-test modules
### Кратко
- Manufacturing-test firmware получил memory-oriented test modules для SDRAM и QSPI Flash.
- QSPI support появился и как BSP module, и как firmware-side test.
### Добавлено
- Добавлены API и реализация `bsp/sdram`.
- Добавлен `firmware/test/src/tests/test_sdram.c`.
- Добавлен `bsp/qspi_flash` с API, реализацией и README.
@ -177,21 +240,26 @@
- Добавлен `firmware/test/src/tests/README.md` с описанием firmware-side test modules.
### Изменено
- `firmware/test/src/main.c` обновлён для интеграции новых test modules.
### Тесты
- SDRAM и QSPI вошли в firmware-test command model.
### Документация
- Документация по test modules начала описывать растущий firmware-test suite.
## [2026-04-17] — Binary protocol, test runner и hardware documentation
### Кратко
- Manufacturing-test firmware перешёл от простого CLI к protocol-driven test runner architecture.
- Hardware reference documentation существенно расширилась.
### Добавлено
- `protocol.c/.h` ввели binary protocol для управления тестами.
- `test_module.h` и `test_runner.c/.h` ввели modular firmware-test runner.
- Добавлены host tests для protocol и runner behavior.
@ -199,69 +267,85 @@
- Добавлены hardware PDFs и board/display reference materials в документацию.
### Изменено
- `firmware/test/README.md` сильно переписан под новую архитектуру.
- Existing CLI host tests были расширены.
### Тесты
- Host coverage расширился на protocol, runner и CLI behavior.
### Удалено
- Старые planning/architecture artifacts в `firmware/test` и HIL refactor notes удалены после замены новой структурой.
### Отфильтрованный шум
- Большие добавления hardware PDF сведены по назначению, без перечисления каждого файла.
## [2026-04-07 .. 2026-04-09] — USB-CDC CLI и priority queue
### Кратко
- Test firmware получил modular USB-CDC CLI.
- Добавлен priority queue utility и host coverage для него.
### Добавлено
- `firmware/test/src/cli.c` и `cli.h` ввели отдельный CLI module.
- `tests/host/cli/test_cli.c` добавил host coverage для CLI behavior.
- Добавлен `utils/prio_queue` с README и большим host test.
- Появилась placeholder structure для `bsp/display`.
### Изменено
- Старый monolithic `firmware/test/main.c` path заменён на modular source layout.
- Добавлены firmware-test planning docs вокруг CLI/protocol roadmap.
### Тесты
- Host tests покрыли CLI и priority queue.
## [2026-04-03 .. 2026-04-06] — USB-CDC stack и HIL coverage
### Кратко
- USB-CDC ACM стал реальной BSP capability и был подключён к HIL validation.
- HIL configuration и documentation были уточнены вокруг нового USB path.
### Добавлено
- Реализован `bsp/usb_cdc` с API, descriptors, Chapter 9 handling и hardware wrappers.
- SDK USB middleware integration добавлена в сборку.
- `tests/target/hil_usb_cdc` и `tools/hil/05_test_usb_cdc.py` добавили USB-CDC HIL coverage.
### Изменено
- Обработка HIL configuration была отрефакторена, `.env.example` получил новые переменные.
- `bsp/usb_cdc/README.md` переписан с фокусом на API documentation.
### Тесты
- USB-CDC вошёл в numbered HIL suite после UART, opto, CAN и button.
### Документация
- HIL docs, включая bench, creation guide, fixtures и how-to, были обновлены.
## [2026-04-01 .. 2026-04-02] — Clock, pin и MPU setup
### Кратко
- Board generated files стали полнее: в firmware base вошли clock, pin и MPU configuration.
### Добавлено
- Добавлены generated clock и pin-mux configuration files.
- Добавлен `TFT_Board.mex` как project state NXP Config Tools.
- Появились initial empty `bsp/sdram` placeholders.
### Изменено
- Board startup/configuration получил MPU initialization.
- Linker scripts скорректированы для FlexSPI NOR и RAM layout.
- `firmware/test/main.c` упрощён вокруг нового initialization path.
@ -269,94 +353,116 @@
## [2026-03-30 .. 2026-03-31] — CAN и button BSP с host/HIL tests
### Кратко
- CAN и button support стали полноценными BSP modules с host и HIL validation.
- HIL tests были пронумерованы в ordered suite.
### Добавлено
- Добавлен `bsp/can` с API, реализацией, README и mocks.
- Добавлены host tests и HIL target/test code для CAN.
- Добавлен `bsp/button` с API, реализацией и README.
- Добавлены host tests и HIL target/test code для button.
### Изменено
- HIL pytest files переименованы в ordered sequence: UART, opto, CAN, button.
- HIL documentation обновлена по мере конкретизации test suite.
### Тесты
- CAN и button получили host test coverage.
- CAN и button получили HIL coverage.
### Удалено
- Удалён `bsp/can/PLAN.md` после переноса полезного содержания в README.
## [2026-03-26 .. 2026-03-28] — M5StampPLC HIL bench и fixture documentation
### Кратко
- HIL стал больше чем pyOCD prototype: появились M5StampPLC support, power/control helpers и fixture documentation.
### Добавлено
- Добавлена поддержка M5StampPLC в `tools/hil`, включая agent/CLI logic и MicroPython firmware assets.
- HIL support code реорганизован в M5-specific helpers.
- `tests/target/hil_opto` и `tools/hil/test_opto.py` добавили opto HIL coverage.
- `docs/testing/hil/HIL_FIXTURES.md` описал pytest fixtures для HIL.
### Изменено
- `just/host.just` и `tools/hil/conftest.py` существенно расширены под HIL workflows.
- Документация реорганизована в более понятные иерархии hardware, MIMXRT1052 и testing.
### Тесты
- Opto inputs получили HIL-level validation.
### Удалено
- Старые locations CMake/test guides заменены новой иерархией `docs/testing`.
### Отфильтрованный шум
- Перемещения PDF и документов учтены как изменение структуры документации, а не как отдельное content change для каждого файла.
## [2026-03-23] — Logging infrastructure и BSP opto
### Кратко
- Появились logging infrastructure и opto input BSP вместе с host coverage.
### Добавлено
- `port/log` и `utils/log` ввели logging abstractions и UART-oriented logging support.
- Добавлены host tests для logging behavior.
- Добавлен `bsp/opto` с API, реализацией, README, GPIO mocks и host tests.
### Тесты
- Host coverage расширился на logging и opto behavior.
### Удалено
- Корневой `TODO.md` удалён после переноса планирования в другие места.
## [2026-03-18 .. 2026-03-20] — Первый HIL skeleton и flashing/debug docs
### Кратко
- Проект получил первый HIL skeleton и target-side UART validation path.
- Flashing и debugging tooling стали документированными и scriptable.
### Добавлено
- Добавлен `tools/hil` с pytest/pyOCD/pyserial-oriented utilities.
- `tests/target/host_uart` предоставил target firmware для UART HIL validation.
- `tools/host/flash_swd.py` добавил SWD flashing support.
- Добавлены `docs/HOW_TO_DEBUG.md`, расширенные flash docs и tool README.
### Изменено
- Main README и development architecture docs расширены вокруг host/container workflow и testing.
- Добавлены host и HIL test creation guides.
### Тесты
- Появился первый UART HIL path.
### Удалено
- Temporary flash logs удалены после окончания диагностической пользы.
## [2026-03-16 .. 2026-03-17] — Первые BSP modules и host test infrastructure
### Кратко
- Репозиторий получил первые concrete BSP modules и host-test layout.
### Добавлено
- Добавлены `bsp_led` и `bsp_tick` с API, реализацией и README files.
- `bsp/uart_host` добавил LPUART1/MCU-Link VCOM support с API, реализацией, README, mocks и host tests.
- Введены shared BSP status codes.
@ -365,29 +471,36 @@
- `TODO-HIL.md` зафиксировал initial HIL plan.
### Тесты
- Host testing начался с Unity/fff-style mocks и isolated test directories.
## [2026-03-13 .. 2026-03-15] — Draft архитектуры manufacturing-test firmware
### Кратко
- Архитектура manufacturing-test firmware была задокументирована до последующей реализации protocol/runner.
### Добавлено
- `firmware/test/README.md` и `firmware/test/arch.svg` описали первый architecture concept.
- BSP и USB-CDC README зафиксировали early design intent.
### Изменено
- `bootstrap.sh` был упрощён.
### Удалено
- Ранняя VS Code launch configuration удалена при cleanup bootstrap.
## [2026-03-10 .. 2026-03-12] — Project environment, BSP base и HAB flow
### Кратко
- Проект перешёл от пустого scaffold к buildable embedded workspace с generated board support, HAB assets и containerized tooling.
### Добавлено
- Добавлен board support для MIMXRT1052: startup code, generated config, linker scripts и FlexSPI NOR-related assets.
- Добавлен HAB signing/configuration flow для app, bootloader и firmware-test images.
- Добавлены formatting/lint/editor configuration.
@ -396,19 +509,23 @@
- Добавлена development architecture и CMake hints documentation.
### Изменено
- Generated NXP Config Tools content перенесён из `bsp/board` в `bsp/generated`.
- Логика `Justfile` разделена на modules под `just/`.
- Добавлена flashing documentation, bootstrap logic переработана.
### Документация
- Early docs явно отмечали, что CI и tests ещё не покрыты.
## [2026-03-05] — Initial repository scaffold
### Кратко
- Репозиторий инициализирован с базовой metadata и README placeholder.
### Добавлено
- Добавлены `.gitattributes`, `.gitignore` и начальный `README.md`.
## Отфильтрованный шум базового среза

View file

@ -1,59 +1,34 @@
# tft_manufacture_test
Монорепозиторий для **MIMXRT1052CVJ5B**. Содержит три независимых firmware-проекта с общей инфраструктурой сборки, тестирования и инструментарием.
Монорепозиторий для **MIMXRT1052CVJ5B**. Содержит три независимых firmware-проекта
с общей инфраструктурой сборки, тестирования и инструментарием.
> Архитектура рабочего окружения — [docs/DEV_ARCH.md](docs/DEV_ARCH.md)
---
## Три firmware-проекта
## Firmware-проекты
| Проект | Путь | Описание |
| ------------------- | ---------------------- | -------------------------------------------------------------------------------------- |
| Тестовая прошивка | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS |
| Загрузчик | `firmware/bootloader/` | A/B обновление через uSD. Сам обновляется только через USB ROM + blhost / SWD |
| Production прошивка | `firmware/tft_app/` | FreeRTOS + FatFS + бизнес-логика. Обновляется загрузчиком |
| Загрузчик | `firmware/bootloader/` | A/B обновление через uSD. Обновляется только через USB ROM + blhost / SWD |
| Production прошивка | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком |
---
## BSP
| Модуль | Путь | Описание |
| --------------- | ---------------- | --------------------------------------------------------- |
| `bsp_led` | `bsp/led/` | Два UserLed (GPIO3[3], GPIO3[4]) |
| `bsp_tick` | `bsp/tick/` | SysTick / FreeRTOS-совместимый таймер |
| `bsp_uart_host` | `bsp/uart_host/` | LPUART1 — MCU-Link VCOM (J2) |
| `bsp_opto` | `bsp/opto/` | Оптоизолированные входы PS2801-4: EXT_IN1, EXT_IN2, RS_RX |
| `bsp_usb_cdc` | `bsp/usb_cdc/` | USB CDC ACM |
| generated | `bsp/generated/` | NXP Config Tools: pin_mux, clock_config, board, startup |
Описание модулей, правила написания компонентов и CMake-шаблоны — [bsp/README.md](bsp/README.md).
---
## Тестирование
| Уровень | Где | Инструменты | Запуск |
| ---------------- | ------------------------------ | -------------------------------------- | -------------------------------------- |
| Host unit-тесты | `tests/host/` | Unity + fff, clang | `just build::test-host` (devcontainer) |
| HIL target-тесты | `tests/target/` + `tools/hil/` | pyOCD + pyserial + pytest + M5StampPLC | `just host::hil-run` (хост) |
**Host-тесты** запускаются в devcontainer без железа. BSP-модули тестируются через fff-фейки и stub-хедеры.
**HIL-тесты** — каждый тест это пара: C-прошивка с UART CLI (`tests/target/<n>/`) и pytest-файл (`tools/hil/NN_test_<n>.py`). pyOCD загружает ELF в RAM через MCU-Link. Тесты с внешними сигналами управляются через M5StampPLC (реле → оптовходы таргета).
Фактический набор HIL-тестов (`tools/hil/`):
| Файл | Назначение |
| -------------------- | ----------------------------------------- |
| `01_test_uart.py` | UART CLI / MCU-Link VCOM |
| `02_test_opto.py` | Оптовходы EXT_IN1, EXT_IN2, RS_RX |
| `03_test_can.py` | CAN-интерфейс |
| `04_test_button.py` | Пользовательские кнопки |
| `05_test_usb_cdc.py` | USB CDC ACM |
```bash
pytest → uart_cmd() → MCU-Link VCOM → RT1052
pytest → m5.opto_set() → M5StampPLC RLY → EXT_IN1/IN2/RS_RX → RT1052
```
| Уровень | Инструменты | Запуск |
| ---------------- | -------------------------------------- | -------------------------------------- |
| Host unit-тесты | Unity + fff, clang | `just build::test-host` (devcontainer) |
| HIL target-тесты | pyOCD + pyserial + pytest + M5StampPLC | `just host::hil-run` (хост) |
- Как добавить host-тест — [docs/testing/host/HOST_CREATE_TEST.md](docs/testing/host/HOST_CREATE_TEST.md)
- Как добавить HIL-тест — [docs/testing/hil/HIL_CREATE_TEST.md](docs/testing/hil/HIL_CREATE_TEST.md)
@ -78,7 +53,7 @@ just host::hil-run # HIL-тесты
just host::debug-server # GDB-сервер для отладки
```
Прошивка подробно — [docs/HOW_TO_FLASH.md](docs/HOW_TO_FLASH.md)
Прошивка подробно — [docs/HOW_TO_FLASH.md](docs/HOW_TO_FLASH.md)
Отладка подробно — [docs/HOW_TO_DEBUG.md](docs/HOW_TO_DEBUG.md)
---
@ -92,7 +67,8 @@ just host::debug-server # GDB-сервер для отладки
| pyOCD, pyserial, pytest, mpremote | `tools/hil/uv.lock` |
| spsdk (nxpimage, blhost, sdphost) | `tools/host/uv.lock` |
Всё что не меняется — vendored. Сборка работает после `git clone` без интернета (кроме Python-зависимостей).
Всё что не меняется — vendored. Сборка работает после `git clone` без интернета
(кроме Python-зависимостей).
---

View file

@ -7,20 +7,17 @@
## Концепция
BSP — единственное место в монорепо где есть знание о конкретном железе. Все три прошивки работают с периферией только через BSP. Прямых вызовов NXP SDK (`fsl_*.h`) за пределами `bsp/` быть не должно.
BSP — единственное место в монорепо где есть знание о конкретном железе. Все три
прошивки работают с периферией только через BSP. Прямых вызовов NXP SDK (`fsl_*.h`)
за пределами `bsp/` быть не должно.
```bash
firmware/test firmware/bootloader firmware/tft_app
↓ ↓ ↓
┌─────────────────────────────────────────────────────┐
│ BSP │
│ bsp_led bsp_opto bsp_tick bsp_uart_host ... │
└─────────────────────────────────────────────────────┘
↓ ↓ ↓
┌─────────────────────────────────────────────────────┐
│ NXP SDK / middleware │
│ fsl_lpuart fsl_gpio fsl_iomuxc ... │
└─────────────────────────────────────────────────────┘
```mermaid
graph TB
FW["firmware/test · firmware/bootloader · firmware/tft_app"]
BSP["BSP"]
SDK["NXP SDK / middleware\nfsl_lpuart · fsl_gpio · fsl_iomuxc · …"]
FW --> BSP --> SDK
```
---
@ -42,18 +39,18 @@ bsp/
│ └── startup_MIMXRT1052.S
├── common/ # bsp_status_t и общие типы
├── led/ # bsp_led — два UserLed (GPIO3[3], GPIO3[4])
├── tick/ # bsp_tick — SysTick / FreeRTOS-совместимый таймер
├── uart_host/ # bsp_uart_host — LPUART1 (MCU-Link VCOM, J2)
├── led/ # bsp_led — два UserLed (GPIO3[3], GPIO3[4])
├── tick/ # bsp_tick — SysTick / FreeRTOS-совместимый таймер
├── uart_host/ # bsp_uart_host — LPUART1 (MCU-Link VCOM, J2)
│ └── mocks/ # fff-заглушки для host-тестов
├── opto/ # bsp_opto — оптоизолированные входы PS2801-4
├── can/ # bsp_can — FlexCAN2 (трансивер SN65HVD230D)
├── button/ # bsp_button — тактовые кнопки SWT6x6 с debounce
├── display/ # bsp_display — TFT-дисплей
├── usb_cdc/ # bsp_usb_cdc — USB CDC ACM
├── sdram/ # bsp_sdram — внешний SDRAM через SEMC
├── qspi_flash/ # bsp_qspi_flash — QSPI Flash W25Q64/128/256/512
└── sd/ # bsp_sd — SD host-контроллер (USDHC1)
├── opto/ # bsp_opto — оптоизолированные входы PS2801-4
├── can/ # bsp_can — FlexCAN2 (трансивер SN65HVD230D)
├── button/ # bsp_button — тактовые кнопки SWT6x6 с debounce
├── display/ # bsp_display — TFT-дисплей
├── usb_cdc/ # bsp_usb_cdc — USB CDC ACM
├── sdram/ # bsp_sdram — внешний SDRAM через SEMC
├── qspi_flash/ # bsp_qspi_flash — QSPI Flash W25Q64/128/256/512
└── sd/ # bsp_sd — SD host-контроллер (USDHC1)
```
---
@ -73,10 +70,10 @@ target_link_libraries(bsp_<любой_компонент> PUBLIC bsp_board)
### Boot-стратегии — INTERFACE-библиотеки
| Таргет CMake | Сценарий | Кто использует |
|---|---|---|
| `bsp_boot_xip` | XIP — исполнение из Flash | `firmware/test`, `firmware/tft_app` |
| `bsp_boot_ram` | исполнение из ITCM/DTCM | HIL target-прошивки (`tests/target/`) |
| Таргет CMake | Сценарий | Кто использует |
| -------------- | ------------------------- | ------------------------------------- |
| `bsp_boot_xip` | XIP — исполнение из Flash | `firmware/test`, `firmware/tft_app` |
| `bsp_boot_ram` | Исполнение из ITCM/DTCM | HIL target-прошивки (`tests/target/`) |
Подключается явно в каждом проекте:
@ -87,19 +84,19 @@ target_link_libraries(test_hil_opto PRIVATE bsp_board bsp_boot_ram ...)
### Компоненты периферии
| Библиотека | Модуль | README |
|---|---|---|
| `bsp_led` | `led/` | [led/README.md](led/README.md) |
| `bsp_tick` | `tick/` | [tick/README.md](tick/README.md) |
| `bsp_uart_host` | `uart_host/` | [uart_host/README.md](uart_host/README.md) |
| `bsp_opto` | `opto/` | [opto/README.md](opto/README.md) |
| `bsp_can` | `can/` | [can/README.md](can/README.md) |
| `bsp_button` | `button/` | [button/README.md](button/README.md) |
| `bsp_display` | `display/` | [display/README.md](display/README.md) |
| `bsp_usb_cdc` | `usb_cdc/` | [usb_cdc/README.md](usb_cdc/README.md) |
| `bsp_sdram` | `sdram/` | [sdram/README.md](sdram/README.md) |
| Библиотека | Модуль | README |
| ---------------- | ------------- | -------------------------------------------- |
| `bsp_led` | `led/` | [led/README.md](led/README.md) |
| `bsp_tick` | `tick/` | [tick/README.md](tick/README.md) |
| `bsp_uart_host` | `uart_host/` | [uart_host/README.md](uart_host/README.md) |
| `bsp_opto` | `opto/` | [opto/README.md](opto/README.md) |
| `bsp_can` | `can/` | [can/README.md](can/README.md) |
| `bsp_button` | `button/` | [button/README.md](button/README.md) |
| `bsp_display` | `display/` | [display/README.md](display/README.md) |
| `bsp_usb_cdc` | `usb_cdc/` | [usb_cdc/README.md](usb_cdc/README.md) |
| `bsp_sdram` | `sdram/` | [sdram/README.md](sdram/README.md) |
| `bsp_qspi_flash` | `qspi_flash/` | [qspi_flash/README.md](qspi_flash/README.md) |
| `bsp_sd` | `sd/` | [sd/README.md](sd/README.md) |
| `bsp_sd` | `sd/` | [sd/README.md](sd/README.md) |
---
@ -107,7 +104,8 @@ target_link_libraries(test_hil_opto PRIVATE bsp_board bsp_boot_ram ...)
### Граница изоляции
Публичные заголовки (`include/bsp/*.h`) не должны содержать ни одного `#include` из NXP SDK. Снаружи BSP — только стандартные типы C и собственные типы проекта.
Публичные заголовки (`include/bsp/*.h`) не должны содержать ни одного `#include`
из NXP SDK. Снаружи BSP — только стандартные типы C и собственные типы проекта.
```c
/* ПРАВИЛЬНО — bsp/opto/include/bsp/opto.h */

View file

@ -1,49 +1,52 @@
# bsp_button — тактовые кнопки
# bsp_button — тактовые кнопки (SWT6x6)
Чтение двух тактовых кнопок SWT6x6 с программным debounce.
Предоставляет мгновенное сырое чтение, стабильное состояние и одноразовые
события нажатия/отпускания.
Чтение двух тактовых кнопок с программным дебаунсом. Предоставляет мгновенное
сырое чтение, стабильное состояние и одноразовые события нажатия/отпускания.
---
## Аппаратура
| Кнопка | Пин MCU | GPIO | Схема | Нажатие |
| -------------- | ---------- | --------- | ----------------------------- | ------- |
| `BSP_BUTTON_1` | GPIO_B1_14 | GPIO2[30] | SWT6x6, pull-up к 3V3 внешний | LOW |
| `BSP_BUTTON_2` | GPIO_B1_15 | GPIO2[31] | SWT6x6, pull-up к 3V3 внешний | LOW |
| Идентификатор | Сигнал | Пин MCU | Корпус | GPIO | Нажатие |
| -------------- | -------- | ---------- | ------ | --------- | ------- |
| `BSP_BUTTON_1` | TactBut1 | GPIO_B1_14 | C14 | GPIO2[30] | LOW |
| `BSP_BUTTON_2` | TactBut2 | GPIO_B1_15 | B14 | GPIO2[31] | LOW |
Пины настроены в `BOARD_InitPins()` (`generated/pin_mux.c`) как INPUT с включённым
гистерезисом, без внутренней подтяжки (`0x0100B0`). `bsp_button_init()` не трогает
Пины настроены в `BOARD_InitPins()` как INPUT с гистерезисом, без внутренней
подтяжки (`0x0100B0`) — внешний pull-up к 3V3. `bsp_button_init()` не трогает
GPIO — только сбрасывает внутреннее состояние модуля.
---
## Принцип работы
## Архитектура
```bash
GPIO2[30] / GPIO2[31]
GPIO_ReadPinInput() ← вызывается в bsp_button_poll()
debounce (счётчик) ← 4 одинаковых сэмпла подряд = 20 мс
stable state + события ← evt_pressed / evt_released
bsp_button_is_pressed()
bsp_button_get_event_pressed()
bsp_button_get_event_released()
```mermaid
flowchart LR
A["GPIO2[30] / GPIO2[31]"] --> B["GPIO_ReadPinInput()\nв bsp_button_poll()"]
B --> C["счётчик дебаунса\n4 одинаковых сэмпла = 20 мс"]
C --> D["stable state\nevt_pressed / evt_released"]
D --> E["bsp_button_is_pressed()\nbsp_button_get_event_pressed()\nbsp_button_get_event_released()"]
```
- **Polling**`bsp_button_poll()` вызывается снаружи каждые 5 мс. Без прерываний.
- **Debounce** — счётчик подтверждений: 4 одинаковых сэмпла подряд фиксируют
переход. Глитч (смена уровня до набора порога) сбрасывает счётчик.
- **События** — одноразовые флаги `evt_pressed` / `evt_released`, сбрасываются
- **Polling**`bsp_button_poll()` вызывается каждые 5 мс. Без прерываний.
- **Дебаунс** — 4 одинаковых сэмпла подряд (`BUTTON_DEBOUNCE_SAMPLES = 4`,
4 × 5 мс = 20 мс). Глитч сбрасывает счётчик.
- **События** — одноразовые флаги `evt_pressed`/`evt_released`, сбрасываются
при первом обращении через `get_event_*()`.
Максимальная задержка реакции = период поллинга = 5 мс. Для навигации по меню
и производственного теста этого достаточно — порог восприятия задержки UI
около 50100 мс.
---
## API
```c
void bsp_button_init(void);
void bsp_button_poll(void); /* вызывать каждые 5 мс */
bool bsp_button_read(bsp_button_t btn); /* сырое чтение без дебаунса */
bool bsp_button_is_pressed(bsp_button_t btn); /* стабильное состояние */
bool bsp_button_get_event_pressed(bsp_button_t btn); /* одноразовое, сбрасывается при чтении */
bool bsp_button_get_event_released(bsp_button_t btn);
```
---
@ -58,167 +61,98 @@ bsp_button_init();
/* bare-metal — вызывать каждые 5 мс из tick-коллбэка: */
bsp_button_poll();
/* В основном цикле: */
if (**bsp_button_get_event_pressed(BSP_BUTTON_1)**) {
/* В main loop: */
if (bsp_button_get_event_pressed(BSP_BUTTON_1)) {
/* однократное срабатывание по нажатию */
}
if (bsp_button_is_pressed(BSP_BUTTON_2)) {
/* кнопка удерживается */
}
/* FreeRTOS — в таске: */
vTaskDelay(pdMS_TO_TICKS(5));
bsp_button_poll();
if (bsp_button_get_event_pressed(BSP_BUTTON_1)) {
xQueueSend(btn_queue, &btn_event, 0);
}
```
### Startup check (bootloader)
**Startup check (bootloader) — сырое чтение до инициализации:**
```c
/* Сырое чтение без debounce — сразу после board_hw_init(),
до вызова bsp_button_init(). */
/* До bsp_button_init(), сразу после board_hw_init(): */
if (bsp_button_read(BSP_BUTTON_1)) {
/* кнопка удерживается при старте → войти в режим обновления */
/* удерживается при старте → режим обновления */
}
```
---
## API
### `bsp_button_init()`
Сбрасывает внутреннее состояние (счётчики debounce, флаги событий).
GPIO уже настроен в `BOARD_InitPins()` — вызывать после `board_hw_init()`.
### `bsp_button_read(btn)`
Мгновенное сырое чтение пина без debounce. Возвращает `true` если кнопка
нажата прямо сейчас. Предназначено для проверки при старте (bootloader hold-check).
### `bsp_button_poll()`
Один шаг debounce. Вызывать строго каждые 5 мс — из tick-коллбэка
(bare-metal) или таска (FreeRTOS). Обновляет стабильное состояние
и выставляет одноразовые события для обеих кнопок за один вызов.
### `bsp_button_is_pressed(btn)`
Стабильное состояние после debounce. `true` = кнопка удерживается нажатой.
Не сбрасывается при чтении.
### `bsp_button_get_event_pressed(btn)`
Одноразовое событие нажатия. Возвращает `true` один раз после того как
debounce зафиксировал переход в нажатое состояние. **Флаг сбрасывается при вызове.**
### `bsp_button_get_event_released(btn)`
Одноразовое событие отпускания. Возвращает `true` один раз после того как
debounce зафиксировал переход в отпущенное состояние. **Флаг сбрасывается при вызове.**
---
## Логика длинного/короткого нажатия
`bsp_button` намеренно не реализует логику длинного/короткого нажатия —
это интерпретация событий, зависящая от контекста приложения.
В `tft_app` реализуется отдельным модулем `button_handler` поверх событий BSP:
```bash
bsp_button → button_handler → app (меню, навигация)
```
`button_handler` хранит `press_start_ms`, использует `bsp_tick_get_ms()`
и вызывает коллбэк с типом действия (`SHORT_PRESS`, `LONG_PRESS`, `REPEAT`).
Не зависит от железа — тестируется на хосте как категория A (без fff).
---
## Конфигурация debounce
```c
/* bsp/button/src/button.c */
#define BUTTON_DEBOUNCE_SAMPLES 4U /* 4 × 5 мс = 20 мс */
```
При изменении периода поллинга нужно пересчитать `BUTTON_DEBOUNCE_SAMPLES`
чтобы сохранить целевое время debounce (рекомендуется 1530 мс).
---
## Тестирование
### Host unit-тесты
Категория **B**`button.c` вызывает `GPIO_ReadPinInput()` из `fsl_gpio.h`.
SDK-функция мокируется через fff в тестовом файле.
Stub `fsl_gpio.h` уже существует в `tests/host/mocks/`.
SDK-функция мокируется через fff. Stub `fsl_gpio.h` в `tests/host/mocks/`.
```cmake
add_host_test(
NAME test_bsp_button
SOURCES button/test_bsp_button.c
${CMAKE_SOURCE_DIR}/bsp/button/src/button.c
INCLUDES
${CMAKE_SOURCE_DIR}/bsp/button/include
${CMAKE_SOURCE_DIR}/bsp/common/include
MOCKS
${BSP_MOCKS_DIR}
)
```bash
just build::test-host # покрытие: init, дебаунс нажатия/отпускания,
# потребление событий, сброс при глитче, независимость кнопок
```
Покрытие: инициализация, сырое чтение, debounce нажатия/отпускания,
потребление событий, сброс счётчика при глитче, независимость кнопок,
граничные значения индекса.
### HIL-тест (интерактивный)
Кнопки расположены на плате таргета — оператор нажимает вручную по подсказкам.
Оператор нажимает кнопки вручную по подсказкам. Не входит в `hil-run`.
```bash
just host::hil-button # запускать отдельно, не входит в hil-run
just host::hil-button
```
C-прошивка: `tests/target/hil_button/` — CLI через `bsp_uart_host`.
pytest: `tools/hil/04_test_button.py` — помечен `@pytest.mark.interactive`.
Команды CLI прошивки:
| Команда | Ответ | Описание |
| --------------- | --------- | ------------------------------------- |
| `PING` | `PONG` | Проверка канала |
| `READ <idx>` | `1` / `0` | Сырое состояние (без debounce) |
| `STATE <idx>` | `1` / `0` | Стабильное состояние после debounce |
| `READ <idx>` | `1` / `0` | Сырое состояние без дебаунса |
| `STATE <idx>` | `1` / `0` | Стабильное состояние после дебаунса |
| `EVENT_P <idx>` | `1` / `0` | `get_event_pressed`, сбрасывает флаг |
| `EVENT_R <idx>` | `1` / `0` | `get_event_released`, сбрасывает флаг |
---
## Зависимости
## Интеграция
| Зависимость | Тип | Описание |
| ------------ | ------- | -------------------------------------------- |
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_board` | PRIVATE | Транзитивно: `pin_mux.h`, clock, SDK headers |
| `sdk_gpio` | PRIVATE | `fsl_gpio.h``GPIO_ReadPinInput()` |
```c
/* FreeRTOS — из задачи: */
vTaskDelay(pdMS_TO_TICKS(5));
bsp_button_poll();
if (bsp_button_get_event_pressed(BSP_BUTTON_1)) {
xQueueSend(btn_queue, &btn_event, 0);
}
```
Логика длинного/короткого нажатия намеренно не реализована в BSP —
это зона ответственности `button_handler` в `tft_app`:
```bash
bsp_button → button_handler → app (меню, навигация)
```
`button_handler` хранит `press_start_ms`, вызывает коллбэк с типом
`SHORT_PRESS` / `LONG_PRESS` / `REPEAT`. Не зависит от железа — тестируется
на хосте как категория A.
---
## Подключение
## CMake
```cmake
# bsp/CMakeLists.txt
add_subdirectory(button)
# firmware/test/CMakeLists.txt или firmware/tft_app/CMakeLists.txt
target_link_libraries(firmware_test PRIVATE
bsp_board
bsp_tick
bsp_button
)
```
**Зависимости модуля:**
| Зависимость | Тип | Описание |
| ------------ | ------- | -------------------------------------------- |
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_board` | PRIVATE | Транзитивно: `pin_mux.h`, clock, SDK headers |
| `sdk_gpio` | PRIVATE | `fsl_gpio.h``GPIO_ReadPinInput()` |

View file

@ -1,47 +1,93 @@
# bsp_can
# bsp_can — FlexCAN2 (CAN 2.0)
Приём и передача CAN 2.0 фреймов через FlexCAN2 (трансивер SN65HVD230D, разъём P1 контакты 34).
Приём и передача CAN 2.0 фреймов через FlexCAN2 (трансивер SN65HVD230D,
разъём P1 контакты 34). Применяется для обмена с управляющими модулями
по CAN-шине: приём команд, отправка откликов.
Применяется для обмена с управляющими модулями по CAN-шине: приём команд,
отправка откликов. Протоколы на стороне приложения — разный DLC, STD/EXT ID.
---
## Аппаратура
| Сигнал | Пин MCU | Корпус | Интерфейс | Примечание |
| ------- | ------------- | ------ | ----------- | --------------------------- |
| CAN2_TX | GPIO_AD_B0_14 | H14 | FlexCAN2 TX | Трансивер SN65HVD230D, P1/3 |
| CAN2_RX | GPIO_AD_B0_15 | L10 | FlexCAN2 RX | Трансивер SN65HVD230D, P1/4 |
Пины настроены в `BOARD_InitPins()` (`generated/pin_mux.c`).
**Распределение Message Buffers:**
| MB | Назначение |
| ---- | -------------------------------------------------- |
| 0 | Зарезервирован (ERR005829 workaround: inactive TX) |
| 1 | TX — отправка фреймов |
| 217 | RX — до 16 индивидуальных фильтров |
ERR005829 — errata FlexCAN на i.MX RT1050/1052: при гонке TX/RX арбитража
MB 0 может зависнуть. Workaround: MB 0 всегда `kFLEXCAN_TxMbInactive`.
---
## Архитектура
```bash
bsp_can_send() ← blocking polling + таймаут
[FlexCAN2 TX MB1] → SN65HVD230D → CAN bus
```mermaid
flowchart TD
subgraph TX
A["bsp_can_send()"] --> B["FLEXCAN_WriteTxMb()\nMB1"]
B --> C["polling FLEXCAN_GetMbStatusFlags()\nтаймаут"]
C --> D["SN65HVD230D → CAN bus"]
end
[CAN bus] → SN65HVD230D → [FlexCAN2 RX MB2..17]
poll_rx_mailboxes() ← опрос флагов MB
ring_buffer ← внутренний FIFO
bsp_can_receive() ← polling + таймаут
subgraph RX
E["CAN bus → SN65HVD230D"] --> F["FlexCAN2 RX MB217"]
F --> G["poll_rx_mailboxes()"]
G --> H["ring_buffer (внутренний FIFO)"]
H --> I["bsp_can_receive()"]
end
```
- **TX** — blocking polling с таймаутом. `bsp_can_send()` записывает фрейм
в TX MB и ждёт флага завершения. Worst case при 500 kbit/s — ~260 мкс на фрейм.
- **RX** — polling. `bsp_can_receive()` опрашивает все активные RX MB,
складывает найденные фреймы во внутренний ring buffer, возвращает первый
доступный. Без прерываний.
- **TX** — blocking polling с таймаутом. Worst case при 500 kbit/s — ~260 мкс на фрейм.
- **RX** — polling без прерываний. `bsp_can_receive()` опрашивает все активные
RX MB, складывает фреймы в ring buffer, возвращает первый доступный.
- **Singleton** — один экземпляр, одна CAN-шина.
---
## Распределение Message Buffers
## API
| MB | Назначение |
| ----- | -------------------------------------------------- |
| 0 | Зарезервирован (ERR005829 workaround: inactive TX) |
| 1 | TX — отправка фреймов |
| 2..17 | RX — до 16 индивидуальных фильтров |
```c
bsp_status_t bsp_can_init(const bsp_can_config_t *p_cfg);
ERR005829 — errata FlexCAN на i.MX RT1050/1052: при гонке TX/RX арбитража
MB 0 может зависнуть. Workaround: MB 0 всегда `kFLEXCAN_TxMbInactive`.
bsp_status_t bsp_can_send(const bsp_can_frame_t *p_frame, uint32_t timeout_ms);
bsp_status_t bsp_can_receive(bsp_can_frame_t *p_frame, uint32_t timeout_ms);
bsp_status_t bsp_can_set_filter(uint8_t idx, uint32_t id,
uint32_t mask, bool is_extended);
bsp_status_t bsp_can_accept_all(void);
bsp_status_t bsp_can_register_rx_callback(bsp_can_rx_cb_t cb, void *p_ctx);
```
**Коды возврата `bsp_can_send()`:**
| Код | Условие |
| ----------------- | -------------------------------- |
| `BSP_OK` | Фрейм успешно отправлен |
| `BSP_ERR_BUSY` | TX MB занят предыдущей передачей |
| `BSP_ERR_TIMEOUT` | Истёк `timeout_ms` |
| `BSP_ERR_PARAM` | Невалидные параметры |
**Коды возврата `bsp_can_receive()`:**
| Код | Условие |
| ----------------- | -------------------- |
| `BSP_OK` | Фрейм получен |
| `BSP_ERR_TIMEOUT` | Истёк `timeout_ms` |
| `BSP_ERR_PARAM` | Невалидные параметры |
`bsp_can_register_rx_callback()` в текущей версии возвращает
`BSP_ERR_NOT_SUPPORTED` — API заложен для будущей интеграции с FreeRTOS
(ISR → `xQueueSendFromISR`).
---
@ -54,7 +100,7 @@ MB 0 может зависнуть. Workaround: MB 0 всегда `kFLEXCAN_TxMb
bsp_can_config_t cfg = { .bitrate = 500000U };
bsp_can_init(&cfg);
/* Принимать всё */
/* Принимать все фреймы */
bsp_can_accept_all();
/* TX — blocking, таймаут 500 мс */
@ -71,129 +117,31 @@ if (bsp_can_receive(&rx, 100) == BSP_OK) {
}
```
---
## Фильтрация
Каждый фильтр занимает один RX MB. Максимум 16 фильтров (`BSP_CAN_FILTER_MAX`).
**Фильтрация:**
```c
/* Принимать только STD ID 0x200 с точным совпадением */
/* STD ID 0x200 — точное совпадение */
bsp_can_set_filter(0, 0x200, 0x7FF, false);
/* Принимать STD ID 0x300..0x30F (маска 0x7F0, младшие 4 бита игнорируются) */
/* STD ID 0x3000x30F */
bsp_can_set_filter(1, 0x300, 0x7F0, false);
/* Принимать EXT ID 0x1ABCDEF0 с точным совпадением */
/* EXT ID 0x1ABCDEF0 */
bsp_can_set_filter(2, 0x1ABCDEF0, 0x1FFFFFFF, true);
/* Сбросить фильтры — принимать всё (STD + EXT) */
bsp_can_accept_all();
```
`bsp_can_accept_all()` настраивает два MB: один для STD (маска 0), один
для EXT (маска 0). Все остальные MB деактивируются.
---
## Блокирующее поведение
### TX: `bsp_can_send()`
Блокирующий вызов. Записывает фрейм в TX MB через `FLEXCAN_WriteTxMb()`,
затем ждёт флага завершения с polling `FLEXCAN_GetMbStatusFlags()`.
Возвращается по одному из условий:
| Условие | Возврат |
| -------------------------------- | ----------------- |
| Фрейм успешно отправлен | `BSP_OK` |
| TX MB занят предыдущей передачей | `BSP_ERR_BUSY` |
| Истёк `timeout_ms` | `BSP_ERR_TIMEOUT` |
| Невалидные параметры | `BSP_ERR_PARAM` |
При 500 kbit/s максимальное время отправки одного фрейма — ~260 мкс.
Для bare-metal и FreeRTOS-задачи это приемлемо.
### RX: `bsp_can_receive()`
Polling с таймаутом. Обходит все активные RX MB, читает готовые фреймы
во внутренний ring buffer, пытается извлечь один фрейм:
| Условие | Возврат |
| ------------------------------- | ----------------- |
| Фрейм найден (из буфера или MB) | `BSP_OK` |
| Истёк `timeout_ms` | `BSP_ERR_TIMEOUT` |
| Невалидные параметры | `BSP_ERR_PARAM` |
```c
/* Неблокирующий опрос — timeout_ms = 0 */
if (bsp_can_receive(&rx, 0) == BSP_OK) { /* есть фрейм */ }
/* Ожидание с таймаутом */
bsp_can_receive(&rx, 1000); /* ждать до 1 секунды */
```
---
## Callback-механизм (заглушка)
В первой итерации `bsp_can_register_rx_callback()` возвращает
`BSP_ERR_NOT_SUPPORTED`. API заложен для будущей интеграции с FreeRTOS:
```c
/* Будущее использование в firmware/tft_app: */
static void can_isr_to_queue(const bsp_can_frame_t *p_frame, void *p_ctx)
{
/* xQueueSendFromISR(...) */
}
bsp_can_register_rx_callback(can_isr_to_queue, NULL);
```
При реализации callback включит прерывания на RX MB. ISR читает фрейм
и вызывает callback напрямую. Callback **не должен блокироваться** — только
атомарные операции (флаг, очередь). Polling через `bsp_can_receive()`
отключается при активном callback.
---
## FreeRTOS
Модуль не зависит от FreeRTOS и работает в обоих контекстах:
| Контекст | TX | RX |
| --------------------------------- | ----------------------------------- | ---------------------------------------------- |
| bare-metal (`firmware/test`, HIL) | `bsp_can_send()` — blocking polling | `bsp_can_receive()` — polling |
| FreeRTOS (`firmware/tft_app`) | `bsp_can_send()` — из задачи | `bsp_can_receive()` — из задачи с `timeout_ms` |
Для FreeRTOS с минимальной латентностью — будущий callback + `xQueueSendFromISR()`.
Polling с `timeout_ms = 10` из задачи подходит для протоколов с интервалом > 10 мс.
---
## Подключение
```cmake
# bsp/CMakeLists.txt
add_subdirectory(can)
# firmware/test/CMakeLists.txt
target_link_libraries(firmware_test PRIVATE
bsp_board
bsp_tick
bsp_can
)
```
Каждый фильтр занимает один RX MB. Максимум 16 фильтров (`BSP_CAN_FILTER_MAX`).
`bsp_can_accept_all()` настраивает два MB (STD + EXT с маской 0),
деактивирует остальные.
---
## Тестирование
### Host unit-тесты (тестирование логики bsp_can)
### Host unit-тесты
Категория **B** — модуль вызывает NXP SDK (`fsl_flexcan.h`).
SDK-функции мокаются через fff в тестовом файле.
Stubs: `fsl_flexcan.h`, `fsl_common.h`, `clock_config.h` в `tests/host/mocks/`.
Категория **B**`bsp_can.c` вызывает `fsl_flexcan.h`. SDK-функции мокируются
через fff. Stub `fsl_flexcan.h` в `tests/host/mocks/`.
```cmake
add_host_test(
@ -205,39 +153,60 @@ add_host_test(
${PROJECT_SOURCE_DIR}/bsp/can/include
${PROJECT_SOURCE_DIR}/bsp/common/include
${PROJECT_SOURCE_DIR}/utils/ring_buffer
MOCKS
${BSP_MOCKS_DIR}
MOCKS ${BSP_MOCKS_DIR}
)
```
### Humble Object (тестирование потребителей bsp_can)
Модуль предоставляет fff-заглушки в `bsp/can/mocks/`:
**Humble Object** — fff-заглушки для потребителей в `bsp/can/mocks/`:
```c
#include "fff.h"
DEFINE_FFF_GLOBALS;
#include "bsp/can.h"
#include "can_mock.h"
void setUp(void) { CAN_MOCK_RESET_ALL(); }
void test_protocol_sends_response(void) {
bsp_can_send_fake.return_val = BSP_OK;
/* ... вызываем protocol_handle_frame() ... */
/* ... */
TEST_ASSERT_EQUAL(1, bsp_can_send_fake.call_count);
}
```
### HIL-тесты
C-прошивка `tests/target/can/` с CLI через UART + pytest `tools/hil/test_can.py`.
CAN-адаптер на стороне хоста — M5Stack с CAN-модулем.
C-прошивка: `tests/target/hil_can/` — CLI через `bsp_uart_host`.
pytest: `tools/hil/03_test_can.py` — CAN-адаптер на M5Stack с CAN-модулем.
```bash
just host::hil-can
```
---
## Зависимости
## Интеграция
Модуль работает в обоих контекстах без изменений:
| Контекст | TX | RX |
| ----------------------------- | ---------------------------- | ------------------------------------- |
| bare-metal (`firmware/test`) | `bsp_can_send()` — blocking | `bsp_can_receive()` — polling |
| FreeRTOS (`firmware/tft_app`) | `bsp_can_send()` — из задачи | `bsp_can_receive()` из задачи с yield |
Для FreeRTOS с минимальной латентностью — будущий callback + `xQueueSendFromISR()`.
---
## CMake
```cmake
# firmware/test/CMakeLists.txt
target_link_libraries(firmware_test PRIVATE
bsp_board
bsp_tick
bsp_can
)
```
**Зависимости модуля:**
| Зависимость | Тип | Описание |
| -------------- | ------- | -------------------------------------- |

View file

@ -1,96 +1,51 @@
# bsp_display — ELCDIF RGB-дисплей (TFT4 / TFT7 / TFT8 / TFT10)
# bsp_display — ELCDIF RGB-дисплей (TFT7 / TFT8 / TFT4 / TFT10)
> Расположение: `bsp/display/`
> Публичный заголовок: `bsp/display/include/bsp/display.h`
> Реализация: `bsp/display/src/display.c`
Модуль инициализирует ELCDIF в RGB-режиме, настраивает пиксельный клок,
управляет GPIO подсветки и пинами ориентации/режима LR, UD, MODE, DITHB.
Предоставляет минимальное API для смены фреймбуфера, поворота и оповещения
о завершении кадра через callback. Bare-metal совместим (без FreeRTOS).
Инициализация ELCDIF в RGB-режиме, настройка пиксельного клока, управление
GPIO подсветки и пинами ориентации/режима. Предоставляет API для смены
фреймбуфера, поворота и нотификации о завершении кадра через ISR-safe callback.
Bare-metal совместим, без FreeRTOS.
---
## Аппаратный контекст
## Аппаратура
| Сигнал / параметр | Аппаратное назначение |
| ----------------- | ------------------------------------------------------------------------------------------------ |
| ELCDIF | NXP ELCDIF, RGB-режим, формат пикселя `kELCDIF_PixelFormatXRGB8888`, шина `kELCDIF_DataBus24Bit` |
| Подсветка | `GPIO1[20]` — active-high |
| LR (горизонт.) | `GPIO1[28]` (`Lcdlr_value`) |
| MODE | `GPIO1[29]` — HIGH = DE mode (обязательно для ELCDIF) |
| UD (вертикаль) | `GPIO1[30]` |
| DITHB | `GPIO1[31]` — HIGH = dithering disable (IC default) |
| Пиксельный клок | PLL2 (`mux=0`) для TFT7/TFT8; Video PLL (`mux=2`) для TFT4 |
| IRQ | `LCDIF_IRQHandler` в ITCM, приоритет `DISPLAY_IRQ_PRIORITY = 2` |
**Управляющие GPIO:**
`IOMUXC` конфигурируется в `BOARD_InitPins()` за пределами модуля — здесь
выполняется только `GPIO_PinWrite`.
| Сигнал | Пин MCU | Корпус | GPIO | Назначение |
| ------------ | ------------- | ------ | --------- | ---------------------------- |
| LcdLed | GPIO_AD_B1_04 | L12 | GPIO1[20] | Подсветка, active-high |
| LcdLR (SHLR) | GPIO_AD_B1_12 | H12 | GPIO1[28] | Горизонтальное направление |
| LcdMode | GPIO_AD_B1_13 | H11 | GPIO1[29] | HIGH = DE mode (обязательно) |
| LcdUD (UPDN) | GPIO_AD_B1_14 | G12 | GPIO1[30] | Вертикальное направление |
| LcdDithb | GPIO_AD_B1_15 | J14 | GPIO1[31] | HIGH = dithering disable |
Делители пиксельного клока (исходный код, PLL2 = 528 МГц):
**RGB-интерфейс ELCDIF:**
| Дисплей | clk_mux | pre_div | div | Эффективная частота |
| ------- | --------- | -------- | --- | ------------------------------ |
| TFT7 | PLL2 | 2 | 4 | `528/3/5 = 35.2 МГц` |
| TFT8 | PLL2 | 2 | 3 | `528/3/4 = 44.0 МГц` |
| TFT4 | Video PLL | — (TODO) | — | требует `CLOCK_InitVideoPll` |
| TFT10 | — | — | — | таблица не заполнена (`{ 0 }`) |
| Сигнал | Пины MCU | Примечание |
| ------------ | ---------------- | ------------------------------- |
| LCDIF_CLK | GPIO_B0_00 (D7) | |
| LCDIF_ENABLE | GPIO_B0_01 (E7) | |
| LCDIF_HSYNC | GPIO_B0_02 (E8) | |
| LCDIF_VSYNC | GPIO_B0_03 (D8) | |
| DATA[07] | GPIO_B0_04B0_11 | C8, B8, A8, A9, B9, C9, D9, A10 |
| DATA[815] | GPIO_B0_12B1_03 | |
| DATA[1623] | GPIO_B1_04B1_11 | |
Тайминги HSW/HFP/HBP/VSW/VFP/VBP заданы константами в `display.c`
(`DISPLAY_TFT7_*`, `DISPLAY_TFT8_*`, `DISPLAY_TFT4_*`).
Пины настроены в `BOARD_InitPins()`. `LCDIF_IRQHandler` размещён в ITCM
(`AT_QUICKACCESS_SECTION_CODE`), приоритет `DISPLAY_IRQ_PRIORITY = 2`.
**Пиксельный клок (PLL2 = 528 МГц):**
| Дисплей | clk_mux | pre_div | div | Частота |
| ------- | --------- | ------- | --- | ----------------------------------- |
| TFT7 | PLL2 | 2 | 4 | 528 / 3 / 5 = **35.2 МГц** |
| TFT8 | PLL2 | 2 | 3 | 528 / 3 / 4 = **44.0 МГц** |
| TFT4 | Video PLL | — | — | требует `CLOCK_InitVideoPll` (TODO) |
| TFT10 | — | — | — | зарезервировано |
---
## Состав модуля
```bash
bsp/display/
├── include/bsp/display.h # публичный заголовок
├── src/display.c # реализация API + LCDIF_IRQHandler
└── CMakeLists.txt # цель bsp_display
```
`LCDIF_IRQHandler` размещён в ITCM (`AT_QUICKACCESS_SECTION_CODE`) и только
вызывает зарегистрированный callback; состояние модуля он не модифицирует.
---
## Публичные типы
```c
typedef enum bsp_display_type_e {
BSP_DISPLAY_TFT4 = 0U, /* 480 × 272, нет ножек ориентации/MODE/DITHB */
BSP_DISPLAY_TFT7, /* 1024 × 600, LR + UD + MODE + DITHB */
BSP_DISPLAY_TFT8, /* 800 × 600, LR + UD + MODE + DITHB */
BSP_DISPLAY_TFT10, /* зарезервировано, спецификации уточняются */
BSP_DISPLAY_COUNT,
} bsp_display_type_t;
typedef enum bsp_display_rotation_e {
BSP_DISPLAY_ROTATE_0 = 0U, /* LR=1 UD=0 */
BSP_DISPLAY_ROTATE_90, /* LR=1 UD=1 */
BSP_DISPLAY_ROTATE_180, /* LR=0 UD=1 */
BSP_DISPLAY_ROTATE_270, /* LR=0 UD=0 */
} bsp_display_rotation_t;
typedef struct bsp_display_size_s {
uint16_t width;
uint16_t height;
} bsp_display_size_t;
typedef void (*bsp_display_frame_cb_t)(void); /* ISR-safe */
```
Размеры максимального дисплея (для статического выделения буферов):
```c
#define BSP_DISPLAY_MAX_WIDTH 1024U
#define BSP_DISPLAY_MAX_HEIGHT 600U
```
---
## Публичный API
## API
```c
bsp_status_t bsp_display_init(bsp_display_type_t type,
@ -98,231 +53,93 @@ bsp_status_t bsp_display_init(bsp_display_type_t type,
bsp_display_frame_cb_t p_on_frame_done);
bsp_status_t bsp_display_deinit(void);
bsp_status_t bsp_display_set_rotation(bsp_display_rotation_t rotation);
void bsp_display_set_next_buffer(uint32_t framebuffer_addr);
void bsp_display_set_next_buffer(uint32_t framebuffer_addr);
const bsp_display_size_t *bsp_display_get_size(void);
bsp_display_type_t bsp_display_get_type(void);
bsp_display_type_t bsp_display_get_type(void);
```
### `bsp_display_init`
**Типы дисплея:**
Настраивает пиксельный клок (через `CLOCK_SetMux` / `CLOCK_SetDiv`), включает
`kCLOCK_LcdPixel`, поднимает подсветку, для TFT7/TFT8 инициализирует ножки
ориентации (`ROTATE_0`: LR=1, UD=0) и режима (MODE=1: DE-mode, DITHB=1),
конфигурирует ELCDIF через `ELCDIF_RgbModeInit` и запускает его через
`ELCDIF_RgbModeStart`. Включает прерывание `kELCDIF_CurFrameDoneInterruptEnable`
с приоритетом `DISPLAY_IRQ_PRIORITY = 2`.
```c
typedef enum {
BSP_DISPLAY_TFT4 = 0U, /* 480 × 272 */
BSP_DISPLAY_TFT7, /* 1024 × 600 */
BSP_DISPLAY_TFT8, /* 800 × 600 */
BSP_DISPLAY_TFT10, /* зарезервировано */
BSP_DISPLAY_COUNT,
} bsp_display_type_t;
```
Требования к аргументам и поведение:
**Ротация → LR/UD (TFT7/TFT8):**
- `framebuffer_addr` — физический адрес первого фреймбуфера. Согласно
заголовку, ожидается выравнивание по 64 байтам и размещение в NonCacheable
SDRAM. (Само значение модуль не проверяет — это контракт потребителя.)
- `p_on_frame_done` — ISR-safe callback; `NULL` означает «без callback».
Сохраняется до включения IRQ, чтобы избежать гонки.
- Повторный вызов без `bsp_display_deinit()` — идемпотентен, возвращает
`BSP_OK` без побочных эффектов.
```c
typedef enum {
BSP_DISPLAY_ROTATE_0 = 0U, /* LR=1, UD=0 */
BSP_DISPLAY_FLIP_VERTICAL, /* LR=1, UD=1 */
BSP_DISPLAY_FLIP_BOTH, /* LR=0, UD=1 */
BSP_DISPLAY_FLIP_HORIZONTAL, /* LR=0, UD=0 */
} bsp_display_rotation_t;
```
Коды возврата:
**Коды возврата `bsp_display_init()`:**
| Код | Когда |
| ----------------------- | --------------------------------------------------------------------------------------------------------- |
| `BSP_OK` | Дисплей инициализирован (или уже был инициализирован). |
| `BSP_ERR_PARAM` | `type >= BSP_DISPLAY_COUNT`. |
| `BSP_ERR_NOT_SUPPORTED` | `type` требует Video PLL (TFT4) — `init_pixelclock` возвращает ошибку до реализации `CLOCK_InitVideoPll`. |
| Код | Условие |
| ----------------------- | ------------------------------ |
| `BSP_OK` | Инициализирован (или уже был) |
| `BSP_ERR_PARAM` | `type >= BSP_DISPLAY_COUNT` |
| `BSP_ERR_NOT_SUPPORTED` | TFT4 — Video PLL не реализован |
> Поведение для `BSP_DISPLAY_TFT10` целостно не описано в коде: запись в
> `K_HW_CFG[BSP_DISPLAY_TFT10]` сделана как `{ 0 }`. Использовать TFT10 как
> рабочий параметр сейчас не гарантируется — типу зарезервировано место в
> enum.
`bsp_display_init()` идемпотентен: повторный вызов без `deinit` возвращает
`BSP_OK` без побочных эффектов. Callback `p_on_frame_done` должен быть
ISR-safe (`NULL` допускается).
### `bsp_display_deinit`
Останавливает ELCDIF (`ELCDIF_RgbModeStop`), выключает IRQ, вызывает
`ELCDIF_Deinit`, отключает `kCLOCK_LcdPixel` и гасит подсветку. Безопасен
при вызове до `init` и повторно. Всегда возвращает `BSP_OK`.
### `bsp_display_set_rotation`
Переключает ориентацию через ножки LR/UD для дисплеев с
`has_orientation_pins = true` (TFT7/TFT8). Для TFT4 поддерживается только
`BSP_DISPLAY_ROTATE_0`; другие значения возвращают `BSP_ERR_NOT_SUPPORTED`.
До `bsp_display_init()` возвращает `BSP_ERR_INIT`. Неизвестная rotation —
`BSP_ERR_PARAM`.
Соответствие rotation → LR/UD (из `display.c`):
| Rotation | LR | UD |
| ----------------------------- | --- | --- |
| `BSP_DISPLAY_ROTATE_0` | 1 | 0 |
| `BSP_DISPLAY_FLIP_VERTICAL` | 1 | 1 |
| `BSP_DISPLAY_FLIP_BOTH` | 0 | 1 |
| `BSP_DISPLAY_FLIP_HORIZONTAL` | 0 | 0 |
### `bsp_display_set_next_buffer`
Тонкая обёртка над `ELCDIF_SetNextBufferAddr`. Согласно заголовку, безопасна
из ISR и из задачи; переключение произойдёт аппаратно по окончании текущего
кадра. Возврата нет.
### `bsp_display_get_size` / `bsp_display_get_type`
Возвращают зафиксированные при `init` параметры активного дисплея.
`bsp_display_get_size()` возвращает `NULL` до `init`; `bsp_display_get_type()`
возвращает `BSP_DISPLAY_COUNT`, если дисплей не инициализирован.
### Callback `bsp_display_frame_cb_t`
Вызывается из `LCDIF_IRQHandler` при флаге `kELCDIF_CurFrameDone`. Должен
быть ISR-safe: запись в `volatile`, `xSemaphoreGiveFromISR()` и т.п.; любые
блокирующие операции запрещены (требование из заголовка).
`bsp_display_set_next_buffer()` безопасна из ISR и из задачи; переключение
происходит аппаратно по окончании текущего кадра.
---
## Порядок использования
## Быстрый старт
```c
#include "bsp/display.h"
/* Фреймбуфер — статический, в NonCacheable SDRAM, выровнен по 64 байтам. */
/* Фреймбуфер — в NonCacheable SDRAM, выровнен по 64 байтам */
static AT_NONCACHEABLE_SECTION_ALIGN(
uint32_t fb[BSP_DISPLAY_MAX_HEIGHT * BSP_DISPLAY_MAX_WIDTH], 64U);
static volatile bool g_frame_done;
static void on_frame_done(void) { g_frame_done = true; } /* ISR-safe */
static void on_frame_done(void) { g_frame_done = true; }
void app_init(void)
{
/* board_hw_init() / CLOCK_*/
/* После board_hw_init(): */
bsp_display_init(BSP_DISPLAY_TFT8, (uint32_t)fb, on_frame_done);
bsp_display_set_rotation(BSP_DISPLAY_ROTATE_0);
bsp_status_t s = bsp_display_init(BSP_DISPLAY_TFT8,
(uint32_t) fb,
on_frame_done);
if (s != BSP_OK) { /* обработать */ }
(void) bsp_display_set_rotation(BSP_DISPLAY_ROTATE_0);
/* Залить буфер и показать кадр */
g_frame_done = false;
bsp_display_set_next_buffer((uint32_t) fb);
while (!g_frame_done) { /* ждать */ }
}
/* Показать кадр */
g_frame_done = false;
bsp_display_set_next_buffer((uint32_t)fb);
while (!g_frame_done) {}
```
---
## Зависимости и CMake
## CMake
```cmake
# bsp/display/CMakeLists.txt
add_library(bsp_display STATIC src/display.c)
target_include_directories(bsp_display
PUBLIC include/
PRIVATE src/)
target_link_libraries(bsp_display
PUBLIC bsp_status
PRIVATE bsp_board sdk_elcdif)
```
- `bsp_status` (PUBLIC) — `bsp_status_t` в публичном API.
- `bsp_board` (PRIVATE) — общие board-уровневые символы (`BOARD_*`, IOMUXC).
- `sdk_elcdif` (PRIVATE) — `fsl_elcdif.h`, тип `elcdif_rgb_mode_config_t`,
функции `ELCDIF_*`, флаги полярности.
Дополнительно `display.c` подключает `fsl_clock.h` (`CLOCK_SetMux`,
`CLOCK_SetDiv`, `CLOCK_EnableClock`, `kCLOCK_LcdifPreMux/PreDiv/Div`,
`kCLOCK_LcdPixel`) и `fsl_gpio.h` (`GPIO_PinWrite`) — символы предоставляются
SDK через транзитивные зависимости.
Цель не собирается при `BUILD_TESTS_HOST=ON` (host-сборка) — ранний
`return()` в `CMakeLists.txt`.
Потребитель (`firmware/test/CMakeLists.txt`):
```cmake
target_link_libraries(firmware_test PRIVATE
...
bsp_display
...
)
target_link_libraries(firmware_test PRIVATE bsp_display)
target_compile_definitions(firmware_test PRIVATE
DISPLAY_TEST_TYPE=BSP_DISPLAY_TFT8
...
)
```
`DISPLAY_TEST_TYPE` — параметр **тест-модуля** (`test_display.c`), а не самого
`bsp_display`; см. ниже.
`DISPLAY_TEST_TYPE` — параметр тест-модуля `test_display.c`, не самого BSP.
---
**Зависимости модуля:**
## Связь с firmware_test (`test_display.c`)
Тест-модуль `firmware/test/src/tests/test_display.c` использует это BSP так:
- Тип дисплея определяется макросом `DISPLAY_TEST_TYPE` (по умолчанию
`BSP_DISPLAY_TFT8`), задаётся через `target_compile_definitions` в
`firmware/test/CMakeLists.txt`.
- В `init` тест-модуль вызывает
`bsp_display_init((bsp_display_type_t) DISPLAY_TEST_TYPE, (uint32_t) g_s_framebuf, display_frame_cb)`,
затем `bsp_display_set_rotation(BSP_DISPLAY_ROTATE_0)`.
- Фреймбуфер — `g_s_framebuf[BSP_DISPLAY_MAX_HEIGHT * BSP_DISPLAY_MAX_WIDTH]`
типа `uint32_t`, выровнен по 64 байтам через `AT_NONCACHEABLE_SECTION_ALIGN`.
- Frame sync — `volatile bool g_s_frame_done`, выставляется в callback
`display_frame_cb` (записывает `true`); сбрасывается перед каждым
`bsp_display_set_next_buffer((uint32_t) g_s_framebuf)`.
- Два этапа теста (фактическое поведение из кода):
- **Этап 1 — Цвет**: четыре шага `step_color(RED, GREEN, BLUE, WHITE)`
с XRGB8888 цветами (`0x00FF0000`, `0x0000FF00`, `0x000000FF`, `0x00FFFFFF`).
Каждый шаг — заливка → ожидание кадра → confirm оператора
с таймаутом `DISPLAY_CONFIRM_TIMEOUT_MS = 15000 мс`.
- **Этап 2 — Ротация**: выполняется только если
`bsp_display_get_type() != BSP_DISPLAY_TFT4`. Заливка «левая половина RED,
правая BLUE», затем confirm для `ROTATE_0`, затем `ROTATE_90` через
`bsp_display_set_rotation()`, после чего восстанавливается `ROTATE_0`.
- В `deinit` вызывается `bsp_display_deinit()`.
Дескриптор тест-модуля:
```c
const test_module_t K_TEST_DISPLAY = {
.id = "display",
.name = "TFT Display RGB888",
.critical = false,
.requires_hil = false,
...
};
```
---
## Ограничения и замечания
- **TFT4 не поддерживается** до реализации Video PLL: `bsp_display_init`
возвращает `BSP_ERR_NOT_SUPPORTED`, в исходнике это явно отмечено как TODO
(`CLOCK_InitVideoPll`). Соответственно, и тест-модуль `display` пропускает
этап ротации для TFT4 и поддерживает только `ROTATE_0` через API.
- **TFT10** присутствует только как зарезервированное значение `enum`
(`K_HW_CFG[BSP_DISPLAY_TFT10] = { 0 }`). Реальные тайминги/делители не
заполнены; конкретное поведение `bsp_display_init(BSP_DISPLAY_TFT10, ...)`
не описано документацией модуля и не гарантируется.
- `bsp_display_init` идемпотентен: повторный вызов без `deinit` возвращает
`BSP_OK` и не перенастраивает аппаратуру. Чтобы переинициализировать с
другим типом или новым адресом фреймбуфера, нужно сначала вызвать
`bsp_display_deinit()`.
- Адрес фреймбуфера должен указывать на NonCacheable память (согласно
заголовку); модуль не делает cache maintenance над буфером.
- `bsp_display_set_next_buffer` возвращает `void` — отсутствие ошибки от
ELCDIF предполагается; верификация переключения буфера — задача
потребителя (например, через FRAME_DONE callback).
- IOMUXC ножек LR/UD/MODE/DITHB и пина подсветки конфигурируется в
`BOARD_InitPins()` за пределами модуля; неправильная конфигурация
IOMUXC проявится отсутствием реакции дисплея, а не возвратом ошибки из
API.
| Зависимость | Тип | Описание |
| ------------ | ------- | ------------------------------------------- |
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_board` | PRIVATE | Транзитивно: `BOARD_*`, IOMUXC |
| `sdk_elcdif` | PRIVATE | `fsl_elcdif.h`, `fsl_clock.h`, `fsl_gpio.h` |

View file

@ -1,25 +1,26 @@
# bsp_led
# bsp_led — пользовательские светодиоды
Драйвер двух пользовательских светодиодов на плате.
Управление двумя пользовательскими светодиодами на плате.
Используется для индикации heartbeat и состояния приложения.
---
## Аппаратная часть
## Аппаратура
| `led_id_t` | Сигнал | GPIO | Pin | Координата | Активный уровень |
| --------------- | ---------- | ----- | --- | ---------- | ---------------- |
| `LED_HEARTBEAT` | `UserLed1` | GPIO3 | 3 | M4 | LOW (0 = горит) |
| `LED_APP` | `UserLed2` | GPIO3 | 4 | P2 | LOW (0 = горит) |
| Идентификатор | Сигнал | Пин MCU | Корпус | GPIO | Активный уровень |
| --------------- | -------- | ------------- | ------ | -------- | ---------------- |
| `LED_HEARTBEAT` | UserLed1 | GPIO_SD_B1_03 | M4 | GPIO3[3] | LOW (0 = горит) |
| `LED_APP` | UserLed2 | GPIO_SD_B1_04 | P2 | GPIO3[4] | LOW (0 = горит) |
Пины сконфигурированы в `generated/pin_mux.h` (MCUXpresso Config Tools).
`INIT_GPIO_VALUE = 1U`оба LED выключены сразу после `led_init()`.
Пины настроены в `BOARD_InitPins()` (`generated/pin_mux.c`) как OUTPUT,
`INIT_GPIO_VALUE = 1U`оба LED выключены сразу после `bsp_led_init()`.
---
## API
```c
void bsp_led_init(void); // вызвать один раз после board_hw_init()
void bsp_led_init(void);
void bsp_led_on(led_id_t id);
void bsp_led_off(led_id_t id);
@ -28,22 +29,24 @@ void bsp_led_set(led_id_t id, bool on);
bool bsp_led_get(led_id_t id);
```
Вызвать `bsp_led_init()` один раз после `board_hw_init()`.
---
## Использование
## Быстрый старт
```c
#include "bsp/led.h"
// инициализация
bsp_led_init();
// heartbeat
/* heartbeat в main loop */
bsp_led_toggle(LED_HEARTBEAT);
// прикладная индикация
bsp_led_on(LED_APP); // пакет принят / тест запущен
bsp_led_off(LED_APP); // сброс
/* индикация события */
bsp_led_on(LED_APP);
/* ... */
bsp_led_off(LED_APP);
```
---
@ -51,20 +54,13 @@ bsp_led_off(LED_APP); // сброс
## CMake
```cmake
target_link_libraries(<target> PRIVATE bsp_led)
target_link_libraries(firmware_test PRIVATE bsp_led)
```
Зависимости: `bsp_board` (PUBLIC, транзитивно), `sdk_gpio` (PRIVATE).
При `BUILD_TESTS_HOST=ON` компонент не собирается — мокается через `fff` на уровне теста.
**Зависимости модуля:**
---
## Файлы
```bash
led/
├── CMakeLists.txt
├── include/led.h # публичный API — без NXP хедеров
├── src/led.c # реализация, fsl_gpio.h только здесь
└── README.md # этот файл
```
| Зависимость | Тип | Описание |
| ------------ | ------- | -------------------------------------------- |
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_board` | PRIVATE | Транзитивно: `pin_mux.h`, clock, SDK headers |
| `sdk_gpio` | PRIVATE | `fsl_gpio.h``GPIO_PinWrite/Read()` |

View file

@ -1,75 +1,85 @@
# bsp_opto — оптоизолированные входы
# bsp_opto — оптоизолированные входы (PS2801-4)
Три оптоизолированных входа на базе PS2801-4. Два канала (`IN1`, `IN2`)
предназначены для детектирования уровня с дебаунсом; третий (`RS`) —
для захвата старт-бита бинарного протокола с минимальной задержкой.
---
## Аппаратура
| Канал | Пин MCU | GPIO | Схема |
|--------------------|------------------|------------|--------------------------|
| `BSP_OPTO_CH_IN1` | GPIO_AD_B1_06 | GPIO1[22] | PS2801-4 + 1K pull-up |
| `BSP_OPTO_CH_IN2` | GPIO_AD_B1_05 | GPIO1[21] | PS2801-4 + 1K pull-up |
| `BSP_OPTO_CH_RS` | GPIO_AD_B1_07 | GPIO1[23] | PS2801-4 + 1K pull-up |
| Канал | Сигнал | Пин MCU | Корпус | GPIO | Логика |
| ----------------- | ------ | ------------- | ------ | --------- | ----------- |
| `BSP_OPTO_CH_IN1` | ExtIn1 | GPIO_AD_B1_06 | J12 | GPIO1[22] | active-HIGH |
| `BSP_OPTO_CH_IN2` | ExtIn2 | GPIO_AD_B1_05 | K12 | GPIO1[21] | active-HIGH |
| `BSP_OPTO_CH_RS` | RsRx | GPIO_AD_B1_07 | K10 | GPIO1[23] | active-HIGH |
**Логика:** active-HIGH. Оптопары неинвертирующие (PS2801-4):
- Пин HIGH (ток есть) → `BSP_OPTO_STATE_ACTIVE`
- Пин LOW (тока нет) → `BSP_OPTO_STATE_INACTIVE`
`BSP_OPTO_CH_RS` — опциональный. Активен только при `rs_as_gpio = true` в конфигурации.
При `rs_as_gpio = false` пин остаётся под управлением `bsp_uart_rs` (LPUART3_RX).
PS2801-4 неинвертирующие: пин HIGH (ток есть) → `BSP_OPTO_STATE_ACTIVE`.
Все три пина принадлежат GPIO1[16..31] → один IRQ: `GPIO1_Combined_16_31_IRQn`.
---
`BSP_OPTO_CH_RS` — двойное назначение пина K10:
## Режимы работы каналов
| Режим | Функция | pin_mux |
| ---------- | ------------- | --------------------- |
| GPIO input | `bsp_opto` | `BOARD_InitRS_GPIO()` |
| LPUART3 RX | `bsp_uart_rs` | `BOARD_InitRS_UART()` |
Каждый канал настраивается независимо через поле `modes[]` конфигурации.
### `BSP_OPTO_MODE_LEVEL` — детектирование уровня (IN1, IN2)
Предназначен для детектирования наличия/отсутствия сигнала с программным дебаунсом.
1. ISR фиксирует timestamp (`bsp_tick_get_ms()`) и raw состояние пина, взводит `pending`.
2. ISR **автоматически переключает направление прерывания** (RISING↔FALLING) после каждого
фронта — оба края сигнала ловятся без дополнительной настройки.
3. `bsp_opto_process()` вызывается из main loop. Если с момента последнего фронта прошло
>= `debounce_ms` — перечитывает пин, сравнивает с `confirmed_state`, вызывает коллбэк.
Начальный фронт выбирается **автоматически** при инициализации по текущему состоянию пина
(LOW → ждём RISING, HIGH → ждём FALLING). Поле `edges[]` для этого режима игнорируется.
Рекомендуемое значение `debounce_ms`: **10 мс** (PS2801-4 response ~50 мкс,
основной источник шума — механические контакты на стенде).
Коллбэк вызывается из контекста **main loop** (не из ISR).
### `BSP_OPTO_MODE_PROTO` — детектирование старт-бита протокола (RS)
Предназначен для приёма бинарных протоколов, где требуется минимальная задержка реакции
на первый фронт (старт-бит).
1. ISR фиксирует фронт и **немедленно вызывает коллбэк** — без дебаунса.
2. После срабатывания прерывание канала **отключается** автоматически.
3. Принимающий модуль (декодер протокола) после обработки пакета вызывает
`bsp_opto_proto_arm()` чтобы взвести прерывание для следующего старт-бита.
Направление фронта задаётся полем `edges[]` и не меняется автоматически
(обычно `BSP_OPTO_EDGE_RISING` для старт-бита).
Коллбэк вызывается **прямо из ISR** — он должен быть ISR-safe:
только взводить флаг или писать в `volatile`-переменную, никакой бизнес-логики.
Одновременное использование невозможно. `rs_as_gpio = true` в конфигурации
активирует канал RS; `false` — пин остаётся под LPUART3.
---
## Использование
## Архитектура
```mermaid
flowchart TD
subgraph MODE_LEVEL["MODE_LEVEL — IN1, IN2"]
A["Фронт на пине"] --> B["GPIO1_Combined_16_31_IRQn\nфиксирует timestamp + raw state\nпереключает направление RISING↔FALLING"]
B --> C["bsp_opto_process()\nв main loop"]
C --> D{"debounce_ms прошло?"}
D -- да --> E["перечитать пин\nсравнить с confirmed_state\nвызвать callback"]
D -- нет --> C
end
subgraph MODE_PROTO["MODE_PROTO — RS"]
F["Старт-бит (фронт)"] --> G["GPIO1_Combined_16_31_IRQn\nнемедленный вызов callback\nотключить прерывание канала"]
G --> H["декодер протокола"]
H --> I["bsp_opto_proto_arm()\nвзвести прерывание снова"]
end
```
- **MODE_LEVEL**: callback вызывается из контекста **main loop** — после подтверждения дебаунсом.
- **MODE_PROTO**: callback вызывается **прямо из ISR** — только атомарные операции.
- После срабатывания канал RS автоматически отключается; повторный вызов
`bsp_opto_proto_arm()` обязателен, иначе канал остаётся неактивным.
---
## API
```c
bsp_status_t bsp_opto_init(const bsp_opto_config_t *p_cfg);
void bsp_opto_process(void); /* вызывать из main loop */
bsp_opto_state_t bsp_opto_read(bsp_opto_ch_t ch);
bsp_status_t bsp_opto_proto_arm(bsp_opto_ch_t ch);
```
`bsp_opto_read()` всегда возвращает `BSP_OPTO_STATE_INACTIVE` для каналов
в `MODE_PROTO` — используй `GPIO_PinRead` напрямую при побитовом сэмплировании.
---
## Быстрый старт
### MODE_LEVEL (IN1, IN2)
```c
#include "bsp/opto.h"
static void on_level_change(bsp_opto_ch_t ch, bsp_opto_state_t state)
{
if (ch == BSP_OPTO_CH_IN1 && state == BSP_OPTO_STATE_ACTIVE) {
/* IN1 активирован */
}
if (ch == BSP_OPTO_CH_IN1 && state == BSP_OPTO_STATE_ACTIVE) { /* ... */ }
}
bsp_opto_config_t cfg = {
@ -81,31 +91,22 @@ bsp_opto_config_t cfg = {
};
bsp_opto_init(&cfg);
/* в main loop: */
for (;;) {
bsp_opto_process();
}
```
Полинг без коллбэков:
```c
bsp_opto_state_t state = bsp_opto_read(BSP_OPTO_CH_IN1);
```
### MODE_PROTO (RS) совместно с MODE_LEVEL (IN1, IN2)
```c
/* Коллбэк вызывается из ISR — только атомарные операции */
static volatile bool s_start_bit_detected = false;
static volatile bool s_start_bit = false;
/* Вызывается из ISR — только volatile-запись */
static void on_rs_start_bit(bsp_opto_ch_t ch, bsp_opto_state_t state)
{
s_start_bit_detected = true;
s_start_bit = true;
}
static void on_level_change(bsp_opto_ch_t ch, bsp_opto_state_t state) { /* ... */ }
bsp_opto_config_t cfg = {
.callbacks = { on_level_change, on_level_change, on_rs_start_bit },
.modes = { BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_PROTO },
@ -115,30 +116,56 @@ bsp_opto_config_t cfg = {
};
bsp_opto_init(&cfg);
/* в main loop: */
for (;;) {
bsp_opto_process(); /* обслуживает IN1, IN2 */
bsp_opto_process();
if (s_start_bit_detected) {
s_start_bit_detected = false;
/* запустить декодер протокола по таймеру... */
/* по завершении приёма пакета — взвести для следующего старт-бита */
bsp_opto_proto_arm(BSP_OPTO_CH_RS);
if (s_start_bit) {
s_start_bit = false;
/* запустить декодер... */
bsp_opto_proto_arm(BSP_OPTO_CH_RS); /* взвести для следующего старт-бита */
}
}
```
---
## Совместное использование RS_RX
## Тестирование
Пин GPIO_AD_B1_07 может работать в двух режимах:
### Host unit-тесты
| Режим | BSP-модуль | pin_mux функция |
|----------------|-----------------|-------------------------|
| LPUART3 RX | `bsp_uart_rs` | `BOARD_InitRS_UART()` |
| GPIO input | `bsp_opto` | `BOARD_InitRS_GPIO()` |
Категория **B**`bsp_opto.c` вызывает `fsl_gpio.h`. SDK-функции мокируются
через fff. Stub `fsl_gpio.h` в `tests/host/mocks/`.
Одновременно использовать оба нельзя. В `firmware_test` режим выбирается
при инициализации в зависимости от конфигурации теста.
```bash
just build::test-host # покрытие: init, MODE_LEVEL debounce, MODE_PROTO arm/disarm
```
### HIL-тесты
C-прошивка: `tests/target/hil_opto/` — CLI через `bsp_uart_host`.
pytest: `tools/hil/02_test_opto.py` — управление входами через M5StampPLC RLY24.
```bash
just host::hil-opto
```
---
## CMake
```cmake
target_link_libraries(firmware_test PRIVATE
bsp_board
bsp_tick
bsp_opto
)
```
**Зависимости модуля:**
| Зависимость | Тип | Описание |
| ------------ | ------- | -------------------------------------------- |
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для дебаунс-таймаута |
| `bsp_board` | PRIVATE | Транзитивно: `pin_mux.h`, clock, SDK headers |
| `sdk_gpio` | PRIVATE | `fsl_gpio.h` — GPIO IRQ, `GPIO_PinRead()` |

View file

@ -1,215 +1,153 @@
# bsp_qspi_flash — QSPI Flash W25Q64/128/256/512
> Расположение: `bsp/qspi_flash/`
> Публичный заголовок: `bsp/qspi_flash/include/bsp/qspi_flash.h`
> Реализация: `bsp/qspi_flash/src/qspi_flash.c`
Драйвер QSPI Flash на FlexSPI1 с поддержкой четырёх чипов Winbond.
XIP-безопасен: все функции, трогающие FlexSPI IP-регистры, размещены в ITCM
и выполняются под IRQ lock.
---
## Поддерживаемое железо
## Аппаратура
| Чип | JEDEC mfr | JEDEC cap | Размер | LUT-таблица |
|---------|-----------|-----------|--------|-------------|
| W25Q64 | 0xEF | 0x17 | 8 MB | `K_LUT_3B` |
| W25Q128 | 0xEF | 0x18 | 16 MB | `K_LUT_3B` |
| W25Q256 | 0xEF | 0x19 | 32 MB | `K_LUT_4B` |
| W25Q512 | 0xEF | 0x20 | 64 MB | `K_LUT_4B` |
| Параметр | Значение |
| ------------- | ----------------- |
| Интерфейс MCU | FlexSPI1, порт A1 |
Интерфейс MCU: **FlexSPI1, порт A1** (`FLEXSPI_PortA1`).
**Поддерживаемые чипы:**
| Чип | JEDEC mfr | JEDEC cap | Размер | Адресация |
| ------- | --------- | --------- | ------ | --------- |
| W25Q64 | `0xEF` | `0x17` | 8 MB | 3-byte |
| W25Q128 | `0xEF` | `0x18` | 16 MB | 3-byte |
| W25Q256 | `0xEF` | `0x19` | 32 MB | 4-byte |
| W25Q512 | `0xEF` | `0x20` | 64 MB | 4-byte |
---
## XIP-безопасность
## Архитектура
### Проблема
### XIP-безопасность
Прошивка исполняется XIP из Flash через FlexSPI AHB-интерфейс.
CPU непрерывно фетчит инструкции из Flash по AHB — через LUT-слот 0.
Любая IP-команда FlexSPI блокирует AHB-путь на время выполнения.
Если в этот момент CPU попытается фетчить инструкцию из Flash — **HardFault**.
Прошивка исполняется XIP из Flash по AHB. Любая IP-команда FlexSPI блокирует
AHB-путь — если в этот момент CPU фетчит инструкцию из Flash, происходит
**HardFault**.
### Решение: ITCM + IRQ lock
Решение — два уровня защиты:
Все функции, обращающиеся к регистрам FlexSPI, размещены в **ITCM**
(`0x00000000`) через `AT_QUICKACCESS_SECTION_CODE`. ITCM подключён к CPU
по выделенной шине (не AHB), поэтому фетч инструкций из ITCM не конкурирует
с IP-командами FlexSPI.
```mermaid
flowchart LR
A["публичная функция\nbsp_qspi_*(...)"] --> B["IRQ lock\n__get_PRIMASK + DSB + ISB"]
B --> C["AHBCR.PREFETCHEN = 0"]
C --> D["IP-команда FlexSPI\nиз ITCM\n(AT_QUICKACCESS_SECTION_CODE)"]
D --> E["AHBCR.PREFETCHEN = 1"]
E --> F["IRQ unlock\nвосстановить PRIMASK"]
```
Дополнительно, каждая публичная операция выполняется под **IRQ lock**
(`qspi_irq_lock` / `qspi_irq_unlock` с `__get_PRIMASK()` + `DSB` + `ISB`):
это гарантирует, что прерывание не застанет FlexSPI в середине IP-транзакции.
IRQ unlock восстанавливает предыдущий `PRIMASK`, не включает IRQ безусловно —
вызов из уже заблокированного контекста корректен.
**ITCM** (`0x00000000`) подключён к CPU по выделенной шине — фетч инструкций
не конкурирует с AHB. **IRQ lock** гарантирует, что прерывание не застанет
FlexSPI в середине IP-транзакции; `PRIMASK` восстанавливается, не сбрасывается
безусловно — вызов из уже заблокированного контекста корректен.
AHB prefetch отключается (`AHBCR.PREFETCHEN = 0`) перед серией IP-транзакций
и восстанавливается после.
### Обязательный дефайн в CMakeLists потребителя
**Обязательный дефайн в CMakeLists потребителя:**
```cmake
target_compile_definitions(firmware_test PRIVATE
__STARTUP_INITIALIZE_RAMFUNCTION # ← обязательно
__STARTUP_CLEAR_BSS)
__STARTUP_INITIALIZE_RAMFUNCTION # ← без этого ITCM содержит нули → HardFault
__STARTUP_CLEAR_BSS
)
```
Без `__STARTUP_INITIALIZE_RAMFUNCTION` startup-файл NXP SDK не копирует
`CodeQuickAccess` секцию из Flash в ITCM. В ITCM остаются нули. Первый же
вызов любой ITCM-функции вызывает **HardFault**.
### Адресация W25Q256/512
Вместо `Enter 4-Byte Mode (0xB7)` используются dedicated 4-byte opcodes — XIP-слот 0
(24-bit адресация) не изменяется:
| Операция | W25Q64/128 | W25Q256/512 |
| ---------------- | ---------- | ----------- |
| Sector Erase 4KB | `0x20` | `0x21` |
| Block Erase 32KB | `0x52` | `0x5C` |
| Block Erase 64KB | `0xD8` | `0xDC` |
| Quad Page Prog | `0x32` | `0x34` |
| IP Quad Out Read | `0x6B` | `0x6C` |
---
## Стратегия адресации W25Q256/512
### Почему не Enter 4-Byte Mode (0xB7)
FDCB фиксирует XIP-слот 0 в режиме 24-bit адресации на всех чипах.
Переключение чипа командой 0xB7 сломало бы XIP — AHB продолжал бы
посылать 24-bit адреса, чип ждал бы 32-bit → **HardFault**.
### Dedicated 4-byte address opcodes
W25Q256/512 принимают 32-bit адрес через отдельный набор opcodes — без
изменения режима адресации чипа:
| Операция | W25Q64/128 (3-byte) | W25Q256/512 (4-byte) |
|-------------------|---------------------|----------------------|
| Sector Erase 4KB | `0x20` | `0x21` |
| Block Erase 32KB | `0x52` | `0x5C` |
| Block Erase 64KB | `0xD8` | `0xDC` |
| Quad Page Program | `0x32` | `0x34` |
| IP Quad Out Read | `0x6B` | `0x6C` |
Слот 0 (XIP) **не изменяется**. XIP работает непрерывно на всех чипах.
---
## LUT-слоты
| Слот | Константа | Команда | Зависит от чипа |
|------|----------------|-----------------------------|-----------------|
| 0 | (XIP, FDCB) | Quad Read | Нет (не трогаем)|
| 1 | LSEQ_READ_SR1 | Read SR1 (0x05) | Нет |
| 2 | LSEQ_WR_EN | Write Enable (0x06) | Нет |
| 3 | LSEQ_ERASE_4K | Sector Erase (0x20/0x21) | Да |
| 4 | LSEQ_PP_QUAD | Quad Page Prog (0x32/0x34) | Да |
| 5 | LSEQ_JEDEC | Read JEDEC (0x9F) | Нет |
| 6 | LSEQ_READ_SR2 | Read SR2 (0x35) | Нет |
| 7 | LSEQ_WR_SR2 | Write SR2 (0x31) | Нет |
| 8 | LSEQ_READ_SR3 | Read SR3 (0x15) | Нет |
| 9 | LSEQ_ERASE_32K | Block Erase 32KB (0x52/0x5C)| Да |
| 10 | LSEQ_ERASE_64K | Block Erase 64KB (0xD8/0xDC)| Да |
| 11 | LSEQ_IP_READ | Quad Out Read (0x6B/0x6C) | Да |
Слоты 111 обновляются в `bsp_qspi_init()` под конкретный чип.
Слот 0 никогда не изменяется BSP-кодом.
---
## Watermark FIFO
Размер watermark-юнита читается из регистров `IPRXFCR.RXWMRK` и
`IPTXFCR.TXWMRK` в рантайме — не зашит константой. Это гарантирует
корректную работу если FDCB или SDK изменили настройки watermark по
умолчанию.
---
## Публичный API
Все публичные функции размещены в ITCM (`AT_QUICKACCESS_SECTION_CODE`) и
выполняются под IRQ lock.
## API
```c
/* Инициализация — вызвать до bsp_tick_init() и любой другой функции модуля */
/* Инициализация — вызвать до bsp_tick_init() */
bsp_status_t bsp_qspi_init(void);
/* Чтение JEDEC ID (0x9F) */
/* Идентификация */
bsp_status_t bsp_qspi_read_jedec_id(bsp_qspi_jedec_t *p_jedec);
uint32_t bsp_qspi_flash_size(void); /* доступно после init() */
/* Стирание */
bsp_status_t bsp_qspi_erase_sector(uint32_t addr); /* 4KB, ~45 мс */
bsp_status_t bsp_qspi_erase_block_32k(uint32_t addr); /* 32KB, ~120 мс */
bsp_status_t bsp_qspi_erase_block_64k(uint32_t addr); /* 64KB, ~150 мс */
bsp_status_t bsp_qspi_erase_sector(uint32_t addr); /* 4 KB, ~45 мс */
bsp_status_t bsp_qspi_erase_block_32k(uint32_t addr); /* 32 KB, ~120 мс */
bsp_status_t bsp_qspi_erase_block_64k(uint32_t addr); /* 64 KB, ~150 мс */
/* Запись одной страницы (256 байт) */
bsp_status_t bsp_qspi_write_page(uint32_t addr, const uint8_t *p_data); /* ~3 мс */
/* Запись одной страницы (256 байт, адрес выровнен на BSP_QSPI_PAGE_SIZE) */
bsp_status_t bsp_qspi_write_page(uint32_t addr, const uint8_t *p_data); /* ~3 мс */
/* Чтение через IP-команду (не AHB/XIP) */
bsp_status_t bsp_qspi_read(uint32_t addr, uint8_t *p_buf, size_t size);
/* Размер Flash — доступен после bsp_qspi_init() */
uint32_t bsp_qspi_flash_size(void);
```
Все функции возвращают `BSP_OK` при успехе или `BSP_ERR_HW` при ошибке
FlexSPI / неверном аргументе. `BSP_ERR_HW` должен присутствовать в
`bsp/common/include/bsp/status.h`.
Все функции возвращают `BSP_OK` или `BSP_ERR_HW`.
**Выбор операции стирания:**
| Объём | Рекомендация | Время |
| ------------ | ------------------ | -------------- |
| < 32 KB | `erase_sector` 4KB | ~45 мс × N |
| 32 KB 1 MB | `erase_block_32k` | ~120 мс / 32KB |
| > 1 MB | `erase_block_64k` | ~150 мс / 64KB |
Пример: 4 MB через 64KB = 64 × 150 мс ≈ **9.6 с** против 46 с через 4KB.
---
## Порядок инициализации
`bsp_qspi_init()` должна вызываться **до** `bsp_tick_init()`:
## Быстрый старт
```c
/* main.c */
#include "bsp/qspi_flash.h"
/* main.c — порядок инициализации: */
board_hw_init();
bsp_qspi_init(); /* ← сначала QSPI, до SysTick */
bsp_tick_init(); /* ← потом SysTick */
bsp_usb_cdc_init();
bsp_qspi_init(); /* ← до bsp_tick_init() */
bsp_tick_init();
/* Идентификация чипа */
bsp_qspi_jedec_t jedec;
bsp_qspi_read_jedec_id(&jedec);
/* Стереть сектор и записать страницу */
bsp_qspi_erase_sector(0x00010000);
uint8_t page[256] = { /* ... */ };
bsp_qspi_write_page(0x00010000, page);
/* Прочитать обратно */
uint8_t buf[256];
bsp_qspi_read(0x00010000, buf, sizeof(buf));
```
Причина: `bsp_qspi_init()` и все ITCM-функции выполняются под полным
IRQ lock. Если SysTick уже запущен и прерывание сработает в момент
IP-команды — возможен AHB stall. Порядок инициализации устраняет эту
гонку при первом запуске.
---
## Выбор операции стирания
| Объём очистки | Рекомендация | Время |
|---------------|------------------------|-----------------|
| < 32 KB | `erase_sector` (4KB) | пропорционально |
| 32 KB — 1 MB | `erase_block_32k` | ~120 мс / 32KB |
| > 1 MB | `erase_block_64k` | ~150 мс / 64KB |
Пример: 4 MB через 64KB = 64 × 150 мс ≈ **9.6 с**
против 1024 × 45 мс ≈ **46 с** через 4KB.
---
## CMake
```cmake
# bsp/qspi_flash/CMakeLists.txt
target_link_libraries(bsp_qspi_flash
PUBLIC bsp_status
PRIVATE bsp_board sdk_flexspi
)
```
Потребитель (`firmware_test`):
```cmake
target_link_libraries(firmware_test PRIVATE
bsp_qspi_flash
...
)
target_link_libraries(firmware_test PRIVATE bsp_qspi_flash)
target_compile_definitions(firmware_test PRIVATE
__STARTUP_INITIALIZE_RAMFUNCTION # обязательно для ITCM-функций
__STARTUP_INITIALIZE_RAMFUNCTION
__STARTUP_CLEAR_BSS
)
```
---
**Зависимости модуля:**
## Известные ограничения
- `bsp_qspi_write_page()` — строго одна страница (256 байт). Адрес обязан
быть выровнен на `BSP_QSPI_PAGE_SIZE`. Запись через границу страницы не
поддерживается.
- Нет timeout в `qspi_wait_not_busy()`. Зависание из-за дефектного чипа
потребует watchdog reset. Для диагностической прошивки это приемлемо.
- Chip Erase (0xC7) не реализован — слишком деструктивно при XIP-исполнении.
| Зависимость | Тип | Описание |
| ------------- | ------- | ------------------------------------ |
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_board` | PRIVATE | Транзитивно: clock, SDK headers |
| `sdk_flexspi` | PRIVATE | `fsl_flexspi.h` — FlexSPI IP-команды |

View file

@ -1,130 +1,114 @@
# bsp_sd — SD host-контроллер (USDHC1)
Модуль инициализирует SD host-контроллер и проверяет наличие карты.
Файловой системой не занимается — это ответственность `bsp_usd` (поверх) или
приложения напрямую.
## Место в архитектуре
Каждый слой знает только о слое ниже — зависимости не пересекают границы.
```bash
firmware_test / tft_app
bsp_usd ← монтирование FatFS, тест R/W
├──► firmware_test_fatfs ← ff.c + fsl_sd_disk + diskio (bare-metal ffconf)
│ tft_app_fatfs ← ff.c + fsl_sd_disk + diskio (FreeRTOS ffconf)
│ │
│ ▼
│ port/fatfs/sd ← diskio_sd.c: microsd_disk_* → fsl_sd_disk
│ │
▼ ▼
bsp_sd ← этот модуль: host init / deinit / card detect
bsp/generated/sdmmc_config ← board-level: BOARD_SD_Config, GPIO питания, pad config
sdk_sdmmc_sd ← NXP: fsl_sd, fsl_sdmmc_common, fsl_sdmmc_host (non-blocking)
sdk_usdhc ← NXP HAL: fsl_usdhc
```
Инициализация SD host-контроллера и детект карты. Файловой системой не
занимается — это ответственность слоя `bsp_usd` / `port_fatfs_sd` поверх.
---
## Аппаратный контекст
## Аппаратура
| Сигнал | Пин MCU | Конфигурация |
| ------ | ---------------- | ------------------------------------------------ |
| CLK | GPIO_SD_B0_01 | USDHC1_CLK, периферийный режим |
| CMD | GPIO_SD_B0_00 | USDHC1_CMD, периферийный режим |
| D0D3 | GPIO_SD_B0_0205 | USDHC1_DATA03, периферийный режим |
| CD_B | GPIO_B1_12 | USDHC1_CD_B — детект через GPIO2[28] |
| SdPwr | GPIO_AD_B1_03 | GPIO1[19], active-low, управляется SDK через BSP |
| Сигнал | Пин MCU | Корпус | Конфигурация |
| ------ | ------------- | ------ | --------------------------------------------- |
| CLK | GPIO_SD_B0_01 | J3 | USDHC1_CLK, периферийный режим |
| CMD | GPIO_SD_B0_00 | J4 | USDHC1_CMD, периферийный режим |
| D0 | GPIO_SD_B0_02 | J1 | USDHC1_DATA0, периферийный режим |
| D1 | GPIO_SD_B0_03 | K1 | USDHC1_DATA1, периферийный режим |
| D2 | GPIO_SD_B0_04 | H2 | USDHC1_DATA2, периферийный режим |
| D3 | GPIO_SD_B0_05 | J2 | USDHC1_DATA3, периферийный режим |
| CD_B | GPIO_B1_12 | D13 | USDHC1_CD_B — детект через периферийный режим |
| SdPwr | GPIO_AD_B1_03 | M12 | GPIO1[19], active-low |
**CD_B** подключён как периферийный сигнал USDHC1, а не как GPIO. Детект карты
читается через `USDHC_GetPresentStatusFlags``kUSDHC_CardInsertedFlag`.
GPIO-прерывание на CD не используется (`kSD_DetectCardByHostCD`).
**CD_B** подключён как периферийный сигнал USDHC1 — детект читается через
`USDHC_GetPresentStatusFlags``kUSDHC_CardInsertedFlag`. GPIO-прерывание
не используется (`kSD_DetectCardByHostCD`).
**SdPwr** инициализируется в `BOARD_SD_Config()` как GPIO-выход, выключен при старте.
SDK включает питание автоматически в процессе `SD_HostInit()` через callback.
**SdPwr** инициализируется в `BOARD_SD_Config()` как GPIO-выход, выключен
при старте. SDK включает питание автоматически в `SD_HostInit()` через callback.
---
## Архитектура
```mermaid
graph TB
FW["firmware_test / tft_app"]
USD["bsp_usd\nмонтирование FatFS, тест R/W"]
FATFS["firmware_test_fatfs / tft_app_fatfs\nff.c + fsl_sd_disk + diskio"]
PORT["port/fatfs/sd\ndiskio_sd.c → fsl_sd_disk"]
BSP["bsp_sd\nhost init / deinit / card detect"]
SDMMC_CFG["bsp/generated/sdmmc_config\nBOARD_SD_Config, GPIO питания"]
SDK_SD["sdk_sdmmc_sd\nfsl_sd, fsl_sdmmc_common"]
SDK_USDHC["sdk_usdhc\nfsl_usdhc"]
FW --> USD --> FATFS --> PORT --> BSP
BSP --> SDMMC_CFG --> SDK_SD --> SDK_USDHC
```
**Почему `ff.c` и `fsl_sd_disk.c` не собираются как общая библиотека:**
оба включают `ffconf.h`, который разный для `firmware_test` (`FF_FS_REENTRANT=0`)
и `tft_app` (`FF_FS_REENTRANT=1`). Общий только `port_fatfs_sd` — он `ff.h`
напрямую не включает.
---
## API
### `bsp_sd_init(void)`
Конфигурирует SDMMC host однократно (`BOARD_SD_Config`) и запускает
host-контроллер (`SD_HostInit`).
Повторный вызов без `bsp_sd_deinit` — no-op, возвращает `BSP_OK`.
Возвращает:
- `BSP_OK` — host готов к работе;
- `BSP_ERR_INIT``SD_HostInit` вернул ошибку.
### `bsp_sd_deinit(void)`
Останавливает host-контроллер и отключает питание карты.
Безопасен при вызове до `init` или повторно после `deinit`.
Возвращает:
- `BSP_OK` — всегда.
### `bsp_sd_is_inserted(void)`
Читает регистр `USDHC1 PRSSTAT`. Не требует предварительного `bsp_sd_init()`
включает тактирование USDHC1 самостоятельно через `CLOCK_EnableClock`.
Возвращает:
- `true` — карта вставлена;
- `false` — карта отсутствует.
---
## Разделение ответственности: bsp_sd vs sdmmc_config vs port_fatfs_sd
| Слой | Что делает | Где живёт |
| --------------------- | ------------------------------------------------------- | ----------------------------------- |
| `sdmmc_config` | Константы платы, `BOARD_SD_Config`, GPIO питания, pads | `bsp/generated/` |
| `bsp_sd` | `SD_HostInit/Deinit`, идемпотентность, card detect | `bsp/sd/` |
| `port_fatfs_sd` | `microsd_disk_*``fsl_sd_disk` (FatFS diskio glue) | `port/fatfs/sd/` |
| `firmware_test_fatfs` | `ff.c` + `fsl_sd_disk` + `diskio.c` (bare-metal ffconf) | `firmware/test/fatfs/` |
| `tft_app_fatfs` | `ff.c` + `fsl_sd_disk` + `diskio.c` (FreeRTOS ffconf) | `firmware/tft_app/fatfs/` (будущее) |
**Почему `ff.c` и `fsl_sd_disk.c` не компилируются один раз как общая библиотека:**
оба включают `ff.h``ffconf.h`, который разный для `firmware_test` (bare-metal,
`FF_FS_REENTRANT=0`) и `tft_app` (FreeRTOS, `FF_FS_REENTRANT=1`, `FF_VOLUMES=3`).
Общий только `port_fatfs_sd` — он не включает `ff.h` напрямую.
---
## Зависимости
```cmake
target_link_libraries(bsp_sd
PUBLIC bsp_status # bsp_status_t
PRIVATE bsp_sdmmc_config # BOARD_SD_Config, sdmmc_config.h, sdk_sdmmc_sd
)
```c
bsp_status_t bsp_sd_init(void);
bsp_status_t bsp_sd_deinit(void);
bool bsp_sd_is_inserted(void);
```
`sdk_sdmmc_sd` — транзитивно через `bsp_sdmmc_config`.
`sdk_usdhc` — транзитивно через `sdk_sdmmc_sd`.
**`bsp_sd_init()`** — конфигурирует SDMMC host однократно (`BOARD_SD_Config`)
и запускает host-контроллер (`SD_HostInit`). Повторный вызов без `deinit` — no-op,
возвращает `BSP_OK`.
**`bsp_sd_is_inserted()`** — читает регистр `USDHC1 PRSSTAT`. Не требует
предварительного `bsp_sd_init()` — включает тактирование через `CLOCK_EnableClock`.
Читает аппаратный регистр без дебаунса — добавляй дебаунс в вызывающем коде
при механическом детекте.
**Коды возврата:**
| Функция | Код | Условие |
| --------------- | -------------- | --------------------------- |
| `bsp_sd_init` | `BSP_OK` | Host готов к работе |
| `bsp_sd_init` | `BSP_ERR_INIT` | `SD_HostInit` вернул ошибку |
| `bsp_sd_deinit` | `BSP_OK` | Всегда |
---
## Ограничения
## Быстрый старт
- Модуль рассчитан на одну карту (USDHC1, `g_sd` — единственный дескриптор).
- `bsp_sd_is_inserted()` читает аппаратный регистр без дебаунса. При
механическом детекте возможны ложные срабатывания в момент вставки/извлечения —
добавляй дебаунс в вызывающем коде если нужно.
- Hot-swap не поддерживается: `bsp_sd_deinit()` + `bsp_sd_init()` между сессиями.
```c
#include "bsp/sd.h"
if (!bsp_sd_is_inserted()) {
/* карта отсутствует */
}
if (bsp_sd_init() != BSP_OK) {
/* host не инициализирован */
}
/* работа с картой через FatFS... */
bsp_sd_deinit();
```
---
## CMake
```cmake
target_link_libraries(firmware_test PRIVATE bsp_sd)
```
**Зависимости модуля:**
| Зависимость | Тип | Описание |
| ------------------ | ------- | ----------------------------------------------- |
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_sdmmc_config` | PRIVATE | `BOARD_SD_Config`, sdmmc_config.h, sdk_sdmmc_sd |
`sdk_sdmmc_sd` и `sdk_usdhc` — транзитивно через `bsp_sdmmc_config`.

View file

@ -1,228 +1,105 @@
# bsp_sdram — внешняя SDRAM MT48LC16M16A2 (32 МБ)
> Расположение: `bsp/sdram/`
> Публичный заголовок: `bsp/sdram/include/bsp/sdram.h`
> Реализация: `bsp/sdram/src/sdram.c`
Модуль обеспечивает минимальную верификацию доступности внешней SDRAM,
подключённой к SEMC. Подробное тестирование (паттерны, шина адреса/данных,
retention) выполняется не здесь, а в тест-модуле `firmware_test/test_sdram.c`,
который опирается на константы и API этого модуля.
Минимальная верификация доступности внешней SDRAM, подключённой к SEMC.
Подробное тестирование (паттерны, шина адреса/данных, retention) выполняется
в тест-модуле `firmware_test/test_sdram.c`, который использует константы
и API этого модуля.
---
## Аппаратный контекст
## Аппаратура
| Параметр | Значение |
| --------------------- | ------------------------------------------------ |
| Чип | MT48LC16M16A2 |
| Объём | 32 МБ |
| Ширина шины данных | 16 бит |
| Интерфейс MCU | SEMC, регион BR0 |
| Базовый адрес | `0x80000000` (`BSP_SDRAM_BASE_ADDR`) |
| Конец региона | `0x81FFFFFF` (`+ BSP_SDRAM_SIZE_BYTES = 32 МБ`) |
| Параметр | Значение |
| ------------------ | ------------------------------------ |
| Чип | MT48LC16M16A2 |
| Объём | 32 МБ |
| Ширина шины данных | 16 бит |
| Интерфейс MCU | SEMC, регион BR0 |
| Базовый адрес | `0x80000000` (`BSP_SDRAM_BASE_ADDR`) |
Карта тестового региона (из `sdram.h`):
**Карта тестового региона:**
| Адрес | Назначение |
| ------------ | ---------------------------------------------------------- |
| `0x80000000` | Начало SDRAM (SEMC BR0) |
| `0x80200000` | `BSP_SDRAM_TEST_BASE_ADDR` — база тестового региона |
| `0x81E00000` | Начало non-cacheable региона (USB DMA, 2 MB) |
| `0x81FFFFFF` | Конец SDRAM |
| Адрес | Назначение |
| ------------ | --------------------------------------------------- |
| `0x80000000` | Начало SDRAM (SEMC BR0) |
| `0x80200000` | `BSP_SDRAM_TEST_BASE_ADDR` — база тестового региона |
| `0x81E00000` | Начало non-cacheable региона (USB DMA, 2 MB) |
| `0x81FFFFFF` | Конец SDRAM |
Тестовая база смещена на 2 МБ от начала SDRAM, что согласно комментариям в
заголовке гарантированно выше `.data`/`.bss` прошивки и ниже non-cacheable
региона.
Тестовая база смещена на 2 МБ от начала — гарантированно выше `.data`/`.bss`
прошивки и ниже non-cacheable региона.
**Важно:** SEMC инициализируется через DCD **до вызова `main()`**. Этот модуль
не настраивает SEMC и не трогает его регистры. Если DCD не отработал —
`bsp_sdram_init()` вернёт ошибку, но исправить ситуацию из модуля нельзя.
---
## Архитектурное ограничение: SEMC инициализируется DCD до `main()`
Модуль **не настраивает** контроллер SEMC и не модифицирует его регистры.
Согласно комментарию в `bsp/sdram/CMakeLists.txt` и `sdram.h`, инициализация
SEMC выполнена через **DCD до вызова `main()`**. Регион SDRAM также описан в
MPU как Normal Write-Back cacheable — соответствующая настройка делается за
пределами этого модуля (исходники модуля её не выполняют).
Следствие для верификации: чтобы проверить именно физическую SDRAM, а не
кэш, при readback в реализации используется явный
`SCB_CleanDCache_by_Addr` + `SCB_InvalidateDCache_by_Addr` + `__DSB()`.
---
## Состав модуля
```
bsp/sdram/
├── include/bsp/sdram.h # публичный заголовок
├── src/sdram.c # реализация bsp_sdram_init()
└── CMakeLists.txt # цель bsp_sdram
```
В `sdram.c` определены только статические вспомогательные функции
(`wait_semc_idle`, `flush_cache_at_test_base`, `verify_word`) и одна публичная
функция `bsp_sdram_init()`. Других публичных операций (например, расширенных
тестов памяти) модуль не предоставляет.
---
## Публичные константы
```c
#define BSP_SDRAM_BASE_ADDR 0x80000000UL /* SEMC BR0 */
#define BSP_SDRAM_SIZE_BYTES 0x02000000UL /* 32 MB */
#define BSP_SDRAM_TEST_BASE_ADDR 0x80200000UL /* +2 MB от базы */
#define BSP_SDRAM_TEST_FAST_SIZE 0x00010000UL /* 64 KB */
#define BSP_SDRAM_TEST_FULL_SIZE 0x00100000UL /* 1 MB */
#define BSP_SDRAM_TEST_EXTENDED_SIZE 0x01B00000UL /* 27 MB */
```
Размеры тестов — это **константы для потребителей**; сам `bsp_sdram` не
запускает по ним внутренние проходы. Например, `BSP_SDRAM_TEST_FAST_SIZE`
используется в `firmware_test/test_sdram.c` (фаза «data bus»). Константы
`BSP_SDRAM_TEST_FULL_SIZE` и `BSP_SDRAM_TEST_EXTENDED_SIZE` определены в
заголовке, но их использование текущими потребителями в дереве не описано —
рассматривайте их как ориентиры из описания карты памяти.
---
## Публичный API
## API
```c
bsp_status_t bsp_sdram_init(void);
```
Назначение: верифицировать, что SEMC завершил инициализацию (выполненную DCD)
и что SDRAM отвечает по тестовому адресу.
**Публичные константы:**
Поведение (из `sdram.c`):
```c
#define BSP_SDRAM_BASE_ADDR 0x80000000UL /* SEMC BR0 */
#define BSP_SDRAM_SIZE_BYTES 0x02000000UL /* 32 MB */
#define BSP_SDRAM_TEST_BASE_ADDR 0x80200000UL /* +2 MB */
#define BSP_SDRAM_TEST_FAST_SIZE 0x00010000UL /* 64 KB */
#define BSP_SDRAM_TEST_FULL_SIZE 0x00100000UL /* 1 MB */
#define BSP_SDRAM_TEST_EXTENDED_SIZE 0x01B00000UL /* 27 MB */
```
1. Ждёт перехода SEMC в состояние IDLE по флагу `SEMC->STS0 & SEMC_STS0_IDLE_MASK`,
таймаут — `SDRAM_SEMC_IDLE_TIMEOUT_MS = 10 мс` (через `bsp_tick_get_ms()`).
2. Записывает по `BSP_SDRAM_TEST_BASE_ADDR` паттерн `0xA5A5A5A5`,
Константы размеров — для потребителей; `bsp_sdram` не запускает по ним
внутренних проходов.
**Поведение `bsp_sdram_init()`:**
1. Ждёт перехода SEMC в IDLE (`SEMC->STS0 & SEMC_STS0_IDLE_MASK`),
таймаут 10 мс.
2. Записывает `0xA5A5A5A5` по `BSP_SDRAM_TEST_BASE_ADDR`,
делает `SCB_CleanDCache_by_Addr` + `SCB_InvalidateDCache_by_Addr` + `__DSB()`,
читает обратно и сверяет.
3. Повторяет то же для инверсного паттерна `0x5A5A5A5A`.
4. При успехе устанавливает внутренний флаг готовности и возвращает `BSP_OK`.
3. Повторяет для инверсного паттерна `0x5A5A5A5A`.
Коды возврата:
**Коды возврата:**
| Код | Когда |
| ----------------- | --------------------------------------------------------------------- |
| `BSP_OK` | SDRAM доступна, оба паттерна успешно прочитаны обратно. |
| `BSP_ERR_TIMEOUT` | SEMC не перешёл в IDLE за `SDRAM_SEMC_IDLE_TIMEOUT_MS` (10 мс). |
| `BSP_ERR_INIT` | Хотя бы один паттерн не совпал при readback (DCD/SDRAM не готовы). |
`bsp_sdram_init()` затрагивает только 4 байта по адресу
`BSP_SDRAM_TEST_BASE_ADDR` (две записи 32-битных слов) и не пересекается с
`.data`/`.bss` прошивки благодаря смещению на 2 МБ от базы.
| Код | Условие |
| ----------------- | ------------------------------------------- |
| `BSP_OK` | SDRAM доступна, оба паттерна совпали |
| `BSP_ERR_TIMEOUT` | SEMC не перешёл в IDLE за 10 мс |
| `BSP_ERR_INIT` | Хотя бы один паттерн не совпал при readback |
---
## Порядок использования
## Быстрый старт
```c
#include "bsp/sdram.h"
if (bsp_sdram_init() != BSP_OK) {
/* SEMC/SDRAM недоступны — это критическая ошибка для прошивки,
которая использует SDRAM под фреймбуферы и тестовые регионы. */
handle_critical_error();
}
/* Дальше — обычная работа с памятью по адресам внутри
[BSP_SDRAM_BASE_ADDR, BSP_SDRAM_BASE_ADDR + BSP_SDRAM_SIZE_BYTES). */
/* Работа с памятью по адресам внутри
[BSP_SDRAM_BASE_ADDR, BSP_SDRAM_BASE_ADDR + BSP_SDRAM_SIZE_BYTES) */
```
Полноценные тесты памяти (шина адреса, шина данных, sequential, retention)
запускаются отдельным тест-модулем — см. раздел «Связь с firmware_test».
---
## Зависимости и CMake
## CMake
```cmake
# bsp/sdram/CMakeLists.txt
add_library(bsp_sdram STATIC src/sdram.c)
target_include_directories(bsp_sdram
PUBLIC include/
PRIVATE src/)
target_link_libraries(bsp_sdram
PUBLIC bsp_status
PRIVATE bsp_board bsp_tick sdk_semc)
target_link_libraries(firmware_test PRIVATE bsp_sdram)
```
- `bsp_status` (PUBLIC) — `bsp_status_t` в публичном API.
- `bsp_tick` (PRIVATE) — `bsp_tick_get_ms()` для таймаута SEMC IDLE.
- `sdk_semc` (PRIVATE) — `fsl_semc.h`, нужен для `SEMC->STS0` и
`SEMC_STS0_IDLE_MASK` при проверке готовности контроллера.
- `bsp_board` (PRIVATE) — общие board-уровневые символы.
**Зависимости модуля:**
Цель не собирается при `BUILD_TESTS_HOST=ON` (host-сборка), `CMakeLists.txt`
содержит ранний `return()`.
Потребитель (пример из `firmware/test/CMakeLists.txt`):
```cmake
target_link_libraries(firmware_test PRIVATE
...
bsp_sdram
...
)
```
---
## Связь с firmware_test (`test_sdram.c`)
Тест-модуль `firmware/test/src/tests/test_sdram.c` использует этот BSP как
основу:
- В `init`-фазе модуль вызывает `bsp_sdram_init()` и сохраняет результат
в `g_s_ready`. При неуспехе `run` сразу возвращает FAIL с
`detail = "SEMC not ready — DCD failed?"`.
- Базовый адрес тестового региона берётся из `BSP_SDRAM_TEST_BASE_ADDR`.
- Размер фазы «data bus» — `BSP_SDRAM_TEST_FAST_SIZE` (64 KB).
- Остальные фазы (`address bus`, `sequential`, `retention`) используют
свои локальные константы (`SDRAM_ADDR_BUS_BITS`, `SDRAM_SEQUENTIAL_SIZE`,
`SDRAM_RETENTION_SIZE`), определённые в `test_sdram.c`, не в BSP.
- Cache maintenance в тестовом модуле повторяет ту же схему, что в BSP:
`SCB_CleanDCache_by_Addr` + `SCB_InvalidateDCache_by_Addr` + `__DSB()`.
Дескриптор тест-модуля:
```c
const test_module_t K_TEST_SDRAM = {
.id = "sdram",
.name = "SDRAM 32 MB",
.critical = true,
.requires_hil = false,
...
};
```
---
## Ограничения и замечания
- Модуль **не выполняет** инициализацию SEMC, DCD или MPU. Если соответствующие
механизмы не отработали до `main()`, `bsp_sdram_init()` вернёт ошибку, но
починить ситуацию из этого модуля нельзя — корень проблемы в DCD / startup
/ clock-config.
- Реентрантность `bsp_sdram_init()` не описана и не гарантируется: в прошивке
вызов выполняется однократно на этапе инициализации.
- Тайм-аут IDLE (`10 мс`) рассчитан на здоровый контроллер; в случае реального
отказа SEMC именно эта величина определяет, через сколько `BSP_ERR_TIMEOUT`
будет возвращён.
- Cache maintenance в `verify_word()` работает только по одной кэш-линии
(32 байта), и `BSP_SDRAM_TEST_BASE_ADDR = 0x80200000` подобран кратным
размеру кэш-линии Cortex-M7 — иначе вызовы `SCB_*_by_Addr` потребовали бы
выравнивания.
- Связь между константами размеров (`BSP_SDRAM_TEST_FULL_SIZE`,
`BSP_SDRAM_TEST_EXTENDED_SIZE`) и конкретными сценариями тестирования не
гарантируется этим BSP — это значения из карты памяти, которые потребитель
может использовать или игнорировать. Точная стратегия тестов SDRAM
определена в `firmware_test/test_sdram.c`.
| Зависимость | Тип | Описание |
| ------------ | ------- | -------------------------------------------------- |
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаута SEMC IDLE |
| `sdk_semc` | PRIVATE | `fsl_semc.h``SEMC->STS0`, `SEMC_STS0_IDLE_MASK` |
| `bsp_board` | PRIVATE | Общие board-уровневые символы |

View file

@ -1,82 +1,104 @@
# bsp_tick — интеграция в firmware-проекты
# bsp_tick — системный таймер
`SysTick_Handler` определён внутри `tick.c` и принадлежит модулю `bsp_tick`.
Для добавления внешней логики в обработчик используй слабый хук (см. ниже).
SysTick-based таймер с миллисекундным счётчиком. Используется всеми BSP-модулями
для таймаутов и периодических операций. Совместим с FreeRTOS.
---
## firmware/test и firmware/bootloader (bare-metal)
## API
**CMakeLists.txt**:
```c
void bsp_tick_init(void);
uint32_t bsp_tick_get_ms(void);
void bsp_delay(uint32_t ms);
void bsp_tick_inc(void); /* вызывать из SysTick_Handler / vApplicationTickHook */
```
`bsp_tick_get_ms()` корректно обрабатывает wraparound (`uint32_t` переполняется
через ~49 суток) — используй паттерн с вычитанием:
```c
uint32_t start = bsp_tick_get_ms();
while ((bsp_tick_get_ms() - start) < TIMEOUT_MS) { /* ждать */ }
```
---
## Быстрый старт
```c
#include "bsp/tick.h"
/* Таймаут: */
uint32_t start = bsp_tick_get_ms();
while (!done) {
if ((bsp_tick_get_ms() - start) >= TIMEOUT_MS) { break; }
}
/* Блокирующая пауза: */
bsp_delay(10);
/* Периодическое действие без блокировки: */
static uint32_t s_last_ms = 0U;
if ((bsp_tick_get_ms() - s_last_ms) >= 500U) {
s_last_ms = bsp_tick_get_ms();
/* действие */
}
```
---
## Интеграция
### bare-metal (`firmware/test`, `firmware/bootloader`)
`SysTick_Handler` определён в `tick.c` и принадлежит модулю целиком.
```cmake
target_link_libraries(firmware_test PRIVATE bsp_tick)
```
**Порядок инициализации в main()**:
```c
board_hw_init(); // тактирование и пины (BOARD_BootClockRUN внутри)
bsp_tick_init(); // SysTick — после того как SystemCoreClock актуален
bsp_uart_init(115200); // и далее всё что зависит от времени
/* main.c — порядок инициализации: */
board_hw_init(); /* устанавливает SystemCoreClock */
bsp_tick_init(); /* после board_hw_init() */
```
---
### FreeRTOS (`firmware/tft_app`)
## firmware/tft_app (FreeRTOS)
**FreeRTOSConfig.h** — убедиться:
В FreeRTOS-режиме SysTick захватывается планировщиком. `bsp_tick_init()`
no-op, `bsp_tick_inc()` вызывается из `vApplicationTickHook`.
```c
#define configTICK_RATE_HZ 1000 // 1 тик = 1 мс
#define configUSE_TICK_HOOK 1 // включить vApplicationTickHook
/* FreeRTOSConfig.h */
#define configTICK_RATE_HZ 1000
#define configUSE_TICK_HOOK 1
```
**board.c** — добавить hook (см. раздел про хуки ниже):
```c
/* board.c */
#include "bsp/tick.h"
void vApplicationTickHook(void)
{
bsp_tick_inc(); // no-op в FreeRTOS-режиме, но оставляем для единообразия
bsp_tick_inc();
}
```
**CMakeLists.txt**:
```cmake
target_link_libraries(tft_app PRIVATE bsp_tick freertos_kernel)
target_compile_definitions(tft_app PRIVATE BSP_TICK_FREERTOS_MODE)
```
**Инициализация**:
### Добавление логики в SysTick — weak hook
`tick.c` объявляет `bsp_systick_hook()` с атрибутом `weak`. Определи
эту функцию в любом `.c` файле проекта — линкер подхватит автоматически.
Никаких изменений в `tick.c` не требуется.
```c
board_hw_init();
bsp_tick_init(); // no-op, но вызываем для симметрии с bare-metal
vTaskStartScheduler(); // FreeRTOS берёт SysTick себе здесь
```
---
## Добавление логики в SysTick — weak hook
Каждый BSP-модуль владеет своим прерыванием целиком. `SysTick_Handler`
живёт в `tick.c` и принадлежит `bsp_tick`. Если другому модулю нужно
выполнять работу каждый тик (watchdog, программный таймер и т.п.) —
используй слабый хук, не трогая `tick.c`.
**Как это работает:**
`tick.c` объявляет и вызывает `bsp_systick_hook()` с атрибутом `weak`.
Если никто не определил эту функцию — линкер подставляет пустую
заглушку, накладные расходы нулевые. Как только в любом `.c` файле
проекта появляется сильное определение — оно автоматически подхватывается.
```c
// tick.c (уже реализовано):
__attribute__((weak)) void bsp_systick_hook(void) { /* no-op по умолчанию */ }
/* tick.c (уже реализовано): */
__attribute__((weak)) void bsp_systick_hook(void) {}
void SysTick_Handler(void)
{
@ -85,54 +107,27 @@ void SysTick_Handler(void)
}
```
### **Пример: watchdog из board.c**
```c
// board.c
#include "bsp/tick.h"
#include "bsp/wdog.h"
// Переопределяем слабый хук — линкер возьмёт эту версию
/* board.c — пример: watchdog из хука */
void bsp_systick_hook(void)
{
bsp_wdog_feed();
}
```
### **Пример: два действия в хуке**
```c
// board.c
void bsp_systick_hook(void)
{
bsp_wdog_feed();
bsp_some_other_periodic_task();
}
```
**Важно:** хук вызывается из ISR-контекста. Никаких блокирующих
операций, мьютексов или `bsp_delay()` внутри.
**Важно:** хук вызывается из ISR-контекста. Блокирующие операции, мьютексы
и `bsp_delay()` внутри запрещены.
---
## Использование в BSP-модулях и приложении
## CMake
```c
#include "bsp/tick.h"
// Таймаут (wraparound-safe):
uint32_t start = bsp_tick_get_ms();
while (!done) {
if ((bsp_tick_get_ms() - start) >= TIMEOUT_MS) { break; }
}
// Блокирующая пауза (инициализация, datasheet-задержки):
bsp_delay(10);
// Периодическое действие без блокировки основного цикла:
static uint32_t s_last_ms = 0U;
if ((bsp_tick_get_ms() - s_last_ms) >= 500U) {
s_last_ms = bsp_tick_get_ms();
// ... действие ...
}
```cmake
target_link_libraries(firmware_test PRIVATE bsp_tick)
```
**Зависимости модуля:**
| Зависимость | Тип | Описание |
| ----------- | ------ | ------------------------------ |
| `bsp_board` | PUBLIC | Транзитивно: `SystemCoreClock` |

View file

@ -1,109 +1,153 @@
# bsp_uart_host
# bsp_uart_host — LPUART1 (MCU-Link VCOM)
Коммуникационный канал с хост-машиной через LPUART1 (разъём J2, MCU-Link VCOM).
Применяется для HIL-тестов (pytest + pyserial), отладочного вывода и резервного
Используется для HIL-тестов (pytest + pyserial), отладочного вывода и резервного
канала связи.
---
## Аппаратура
| Сигнал | Пин MCU | Корпус | Интерфейс | Назначение |
| ---------- | ------------- | ------ | ---------- | -------------- |
| LPUART1_TX | GPIO_AD_B0_12 | K14 | LPUART1 TX | MCU → MCU-Link |
| LPUART1_RX | GPIO_AD_B0_13 | L14 | LPUART1 RX | MCU-Link → MCU |
Пины настроены в `BOARD_InitPins()` (`generated/pin_mux.c`).
---
## Архитектура
```bash
[LPUART1 RX] → LPUART1_IRQHandler → ring_buffer_put()
bsp_uart_host_read() ← polling + таймаут
bsp_uart_host_read_byte()
```mermaid
flowchart TD
subgraph TX
A["bsp_uart_host_write()"] --> B["LPUART_WriteBlocking()\nблокирующий polling"]
B --> C["LPUART1 TX → MCU-Link VCOM"]
end
[LPUART1 TX] ← bsp_uart_host_write() ← LPUART_WriteBlocking()
subgraph RX
D["MCU-Link VCOM → LPUART1 RX"] --> E["LPUART1_IRQHandler"]
E --> F["ring_buffer_put()"]
F --> G["bsp_uart_host_read()\nbsp_uart_host_read_byte()\npolling + таймаут"]
end
```
- **TX** — blocking polling (`LPUART_WriteBlocking`). Пакеты короткие, задержка 12 мс приемлема.
- **RX** — ISR пишет в ring buffer, задача/main читает с таймаутом.
- **ISR**`LPUART1_IRQHandler` определён в модуле, модуль владеет прерыванием целиком.
- **TX**`LPUART_WriteBlocking`. Пакеты короткие, задержка 12 мс приемлема.
- **RX** — ISR пишет в ring buffer; main loop читает с таймаутом.
- **ISR**`LPUART1_IRQHandler` определён в модуле, владеет прерыванием целиком.
- **Singleton** — один экземпляр, один физический UART.
---
## API
```c
bsp_status_t bsp_uart_host_init(uint32_t baud);
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);
```
`bsp_uart_host_read()` возвращает фактически прочитанное количество байт —
частичное чтение при таймауте не является ошибкой.
```c
/* Неблокирующий опрос */
bsp_uart_host_read(buf, len, 0);
/* Ждать с таймаутом */
bsp_uart_host_read(buf, len, 100);
/* Ждать вечно */
bsp_uart_host_read(buf, len, BSP_UART_HOST_WAIT_FOREVER);
```
---
## Быстрый старт
```c
#include "bsp/uart_host.h"
// В main(), после board_hw_init():
/* После board_hw_init(): */
bsp_uart_host_init(115200);
// TX
/* TX */
bsp_uart_host_write_str("hello\r\n");
// RX — ждать байт до 100 мс
/* RX — ждать байт до 100 мс */
int32_t byte = bsp_uart_host_read_byte(100);
if (byte < 0) { /* таймаут */ }
// RX — прочитать пакет целиком
/* RX — прочитать пакет */
uint8_t buf[64];
size_t n = bsp_uart_host_read(buf, sizeof(buf), 500);
```
---
## Конфигурация
## Тестирование
Задаётся в CMakeLists.txt **firmware-таргета**, не модуля:
### Host unit-тесты (Humble Object)
Модуль предоставляет fff-заглушки в `bsp/uart_host/mocks/`. В тестовой сборке
вместо `uart_host.c` линкуется `mocks/uart_host_mock.c`.
```cmake
target_compile_definitions(firmware_test PRIVATE
BSP_UART_HOST_RX_BUFFER_SIZE=256 # степень двойки, дефолт 256
BSP_UART_HOST_SRC_CLOCK_HZ=24000000
BSP_UART_HOST_IRQ_PRIORITY=5
add_host_test(
NAME uart_host_mock_example
SOURCES uart_host/test_uart_host.c
${CMAKE_SOURCE_DIR}/bsp/uart_host/mocks/uart_host_mock.c
INCLUDES
${CMAKE_SOURCE_DIR}/bsp/uart_host/include
${CMAKE_SOURCE_DIR}/bsp/uart_host/mocks
${CMAKE_SOURCE_DIR}/bsp/common/include
)
```
| Define | Дефолт | Описание |
| ------------------------------ | ---------- | ---------------------------------------------------------------------------------------------------------- |
| `BSP_UART_HOST_RX_BUFFER_SIZE` | `256` | Размер RX ring buffer. **Должен быть степенью двойки.** |
| `BSP_UART_HOST_SRC_CLOCK_HZ` | `24000000` | Частота источника тактирования LPUART1. |
| `BSP_UART_HOST_IRQ_PRIORITY` | `5` | Приоритет `LPUART1_IRQn`. Должен быть ≥ `configMAX_SYSCALL_INTERRUPT_PRIORITY` при использовании FreeRTOS. |
---
## Таймауты
```c
// Без ожидания — вернёт только то, что уже есть в буфере
bsp_uart_host_read(buf, len, 0);
#include "bsp/uart_host_mock.h"
// Ждать с таймаутом (межбайтовый: сбрасывается после каждого принятого байта)
bsp_uart_host_read(buf, len, 100);
void setUp(void) { UART_HOST_MOCK_RESET_ALL(); }
// Ждать вечно
bsp_uart_host_read(buf, len, BSP_UART_HOST_WAIT_FOREVER);
void test_something(void) {
bsp_uart_host_write_fake.return_val = BSP_OK;
/* ... */
TEST_ASSERT_EQUAL(1, bsp_uart_host_write_fake.call_count);
}
```
`bsp_uart_host_read()` возвращает `size_t` — частичное чтение при таймауте
не является ошибкой, caller сам решает что делать с полученным количеством байт.
### HIL-тесты
C-прошивка: `tests/target/host_uart/` — CLI через LPUART1.
pytest: `tools/hil/01_test_uart.py` — PING/ECHO/BUF_SIZE через pyserial.
```bash
just host::hil-uart
```
---
## FreeRTOS
## Интеграция
Модуль работает в FreeRTOS без отдельной реализации. При сборке с
`BSP_TICK_FREERTOS_MODE` в цикле ожидания добавляется `vTaskDelay(1)`
задача отдаёт управление планировщику вместо busy-wait.
| Контекст | TX | RX |
| ---------- | --------------------- | ------------------------------ |
| bare-metal | `write()` — blocking | `read()` — polling с таймаутом |
| FreeRTOS | `write()` — из задачи | `read()` — из задачи с yield |
`BSP_UART_HOST_IRQ_PRIORITY` должен быть установлен ниже
`configMAX_SYSCALL_INTERRUPT_PRIORITY` (числовое значение выше).
В FreeRTOS-режиме (`BSP_TICK_FREERTOS_MODE`) цикл ожидания добавляет
`vTaskDelay(1)` вместо busy-wait. `BSP_UART_HOST_IRQ_PRIORITY` должен быть
выше `configMAX_SYSCALL_INTERRUPT_PRIORITY` (числовое значение ниже).
---
## Подключение
## CMake
```cmake
# bsp/CMakeLists.txt
add_subdirectory(common)
add_subdirectory(uart_host)
# firmware/test/CMakeLists.txt
target_link_libraries(firmware_test PRIVATE
bsp_board
bsp_tick
@ -111,54 +155,21 @@ target_link_libraries(firmware_test PRIVATE
)
```
---
## Тестирование
Для host unit-тестов модуль предоставляет fff-заглушки через **Humble Object**:
в тестовой сборке вместо `uart_host.c` линкуется `mocks/uart_host_mock.c`.
Конфигурация задаётся в CMakeLists.txt **потребителя**, не модуля:
```cmake
# tests/host/CMakeLists.txt
add_host_test(
NAME
uart_host_mock_example
SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/uart_host/test_uart_host.c
${CMAKE_SOURCE_DIR}/bsp/uart_host/mocks/uart_host_mock.c
# Если тестируете "protocol.c" который использует uart_host:
# ${CMAKE_SOURCE_DIR}/bsp/protocol/src/protocol.c
INCLUDES
${CMAKE_SOURCE_DIR}/bsp/uart_host/include # bsp/uart_host.h
${CMAKE_SOURCE_DIR}/bsp/uart_host/mocks # uart_host_mock.h
${CMAKE_SOURCE_DIR}/bsp/common/include # bsp/status.h
# ${CMAKE_SOURCE_DIR}/bsp/protocol/include # protocol.h
MOCKS
${CMAKE_SOURCE_DIR}/bsp/uart_host/mocks/uart_host_mock.c)
target_compile_definitions(firmware_test PRIVATE
BSP_UART_HOST_RX_BUFFER_SIZE=256 # степень двойки
BSP_UART_HOST_SRC_CLOCK_HZ=24000000
BSP_UART_HOST_IRQ_PRIORITY=5
)
```
```c
#include "fff.h"
DEFINE_FFF_GLOBALS;
#include "bsp/uart_host_mock.h"
void setUp(void) { UART_HOST_MOCK_RESET_ALL(); }
void test_something(void) {
bsp_uart_host_write_fake.return_val = BSP_OK;
// ... вызываем тестируемый код ...
TEST_ASSERT_EQUAL(1, bsp_uart_host_write_fake.call_count);
}
```
---
## Зависимости
**Зависимости модуля:**
| Зависимость | Тип | Описание |
| --------------------- | ------- | --------------------------------- |
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаутов |
| `utils` (ring_buffer) | PRIVATE | RX ring buffer |
| `sdk_lpuart` | PRIVATE | `fsl_lpuart.h`, `fsl_clock.h` |
| `sdk_lpuart` | PRIVATE | `fsl_lpuart.h`, `fsl_clock.h` |

View file

@ -1,123 +1,85 @@
# bsp_usb_cdc — USB CDC ACM (Virtual COM Port)
USB CDC ACM device на USB1 (EHCI0). Хост видит устройство как виртуальный COM-порт
(`/dev/ttyACM*` на Linux/macOS, `COMx` на Windows).
Используется для передачи данных между платой и ПК: отладочные лог-каналы,
CLI команды, обновление конфигурации. Работает параллельно с `bsp_uart_host`
(LPUART1) — два независимых канала.
USB CDC ACM device на USB1 (EHCI0). Хост видит устройство как виртуальный
COM-порт (`/dev/ttyACM*` на Linux/macOS, `COMx` на Windows). Используется
для передачи данных между платой и ПК: CLI команды, отладочные лог-каналы.
Работает параллельно с `bsp_uart_host` (LPUART1) — два независимых канала.
---
## Аппаратура
| Сигнал | Пин MCU | Назначение |
| ------------- | ------------- | -------------------------- |
| USB_OTG1_DN | USB_OTG1_DN | USB1 Data |
| USB_OTG1_DP | USB_OTG1_DP | USB1 Data+ |
| USB_OTG1_VBUS | USB_OTG1_VBUS | VBUS detect (self-powered) |
| Сигнал | Пин MCU | Назначение |
| ------------- | ------------- | ----------- |
| USB_OTG1_DN | USB_OTG1_DN | USB1 Data |
| USB_OTG1_DP | USB_OTG1_DP | USB1 Data+ |
| USB_OTG1_VBUS | USB_OTG1_VBUS | VBUS detect |
Встроенный HS PHY (480 MHz PLL). Контроллер: EHCI0 (`kUSB_ControllerEhci0`).
Скорость: High-Speed (480 Mbit/s) при поддержке хоста, fallback Full-Speed (12 Mbit/s).
Скорость: High-Speed (480 Mbit/s), fallback Full-Speed (12 Mbit/s).
PHY калибровка: `D_CAL=0x0C`, `TXCAL45DP=0x06`, `TXCAL45DM=0x06`.
USB PHY калибровка: `D_CAL=0x0C`, `TXCAL45DP=0x06`, `TXCAL45DM=0x06` — стандартные
значения для EVKB, подходят для кабелей до 3 м.
**VID/PID**: `0x1234` / `0x0001` (placeholder, заменить на производственные).
**VID/PID**: `0x1234` / `0x0001` — placeholder, заменить на производственные.
---
## Архитектура
```bash
bsp_usb_cdc_write()
memcpy → s_sendBuf (NonCacheable OCRAM)
USB_DeviceSendRequest()
[EHCI0 DMA] → USB1_DP/DN → Host
```mermaid
flowchart TD
subgraph TX
A["bsp_usb_cdc_write()"] --> B["memcpy → s_sendBuf\n(NonCacheable OCRAM)"]
B --> C["USB_DeviceSendRequest()"]
C --> D["EHCI0 DMA → USB1_DP/DN → Host"]
D --> E["BulkIn callback\ns_txIdle = 1 (volatile)"]
end
Host → USB1_DP/DN → [EHCI0 DMA]
USB_OTG1_IRQHandler → BulkOut callback
s_recvBuf (NonCacheable OCRAM)
s_recvSize = len (volatile)
bsp_usb_cdc_read() ← main loop polling
subgraph RX
F["Host → USB1_DP/DN → EHCI0 DMA"] --> G["USB_OTG1_IRQHandler\nBulkOut callback"]
G --> H["s_recvBuf (NonCacheable OCRAM)\ns_recvSize = len (volatile)"]
H --> I["bsp_usb_cdc_read()\nmain loop polling"]
end
```
Все DMA-буферы (`s_sendBuf`, `s_recvBuf`, дескрипторы) размещены в секции
`NonCacheable` (OCRAM `0x20200000`). MPU region 9 настраивает эту область
как Normal non-cacheable — записи CPU видны DMA без `SCB_CleanDCache()`.
Все DMA-буферы размещены в секции `NonCacheable` (OCRAM `0x20200000`).
MPU region 9 настраивает эту область как Normal non-cacheable — записи CPU
видны DMA без `SCB_CleanDCache()`.
**Lite stack** — сознательное решение вместо full NXP class framework:
| Аспект | Full stack | Lite stack (наш выбор) |
| --------------- | --------------------- | ---------------------- |
| Class framework | `usb_device_class.h` | Отсутствует |
| Размер кода | ~12 KB | ~6 KB |
| Гибкость | Multi-class composite | Один CDC ACM |
Переход на full stack понадобится только при добавлении composite device (CDC + MSC).
---
## USB стек — lite архитектура
## API
Модуль использует **lite** вариант NXP USB стека (не full class framework).
Это сознательное решение:
```c
bsp_status_t bsp_usb_cdc_init(void);
bool bsp_usb_cdc_is_ready(void);
bool bsp_usb_cdc_write_ready(void);
| Аспект | Full stack | Lite stack (наш выбор) |
| ------------------ | -------------------------------------- | ----------------------------- |
| Class framework | `usb_device_class.h`, `class_handle_t` | Отсутствует |
| `usb_device_ch9.c` | SDK middleware, тянет class driver | Приватная копия в `src/` |
| CDC ACM хедер | Полный: struct + API функции | Только define-ы request codes |
| Callbacks | Через class driver dispatch | Напрямую в `usb_cdc.c` |
| Размер кода | ~12 KB | ~6 KB |
| Гибкость | Multi-class composite | Один CDC ACM |
Lite stack достаточен для одного CDC ACM интерфейса. Переход на full stack
понадобится только при добавлении composite device (CDC + MSC).
### Стек зависимостей
```bash
bsp_usb_cdc
├── src/usb_cdc.c ← BSP API + USB device callbacks
├── src/usb_cdc_descriptors.c ← дескрипторы + descriptor callbacks
├── src/usb_cdc_hw.c ← clock, PHY, IRQ handler
├── src/usb_device_ch9.c ← lite Chapter 9 (приватная копия)
├── SDK (PRIVATE):
│ ├── sdk_usb_device_ehci ← EHCI контроллер + DCI абстракция
│ │ ├── usb_device_ehci.c
│ │ └── usb_device_dci.c
│ ├── sdk_usb_phy ← USB PHY инициализация
│ │ └── usb_phy.c
│ └── sdk_osa_bm ← OS Abstraction (bare-metal)
│ ├── fsl_os_abstraction_bm.c
│ └── fsl_component_generic_list.c
└── Приватные конфиги в src/:
├── usb_device_config.h ← EHCI=1, CDC_ACM=1, endpoints=4
├── fsl_os_abstraction_config.h ← bare-metal OSA конфиг
├── usb_device_descriptor.h ← VID/PID, endpoint numbers
├── usb_device_ch9.h ← lite ch9 API (1 arg)
└── usb_device_cdc_acm.h ← lite: только CDC request codes
bsp_status_t bsp_usb_cdc_write(const uint8_t *p_data, size_t len);
size_t bsp_usb_cdc_read(uint8_t *p_buf, size_t max_len);
void bsp_usb_cdc_poll(void); /* зарезервировано */
```
### Проброс конфиг-хедеров (sdk_usb_config)
**`bsp_usb_cdc_is_ready()`** — `true` когда enumeration завершён **и** хост
открыл COM-порт (DTR установлен через `SET_CONTROL_LINE_STATE`).
NXP USB middleware при компиляции ищет `usb_device_config.h` и
`fsl_os_abstraction_config.h` через include path. Эти файлы —
application-specific, живут в `bsp/usb_cdc/src/`.
**Коды возврата `bsp_usb_cdc_write()`:**
Проблема: SDK таргеты (`sdk_usb_device_ehci`, `sdk_usb_phy`, `sdk_osa_bm`)
компилируются независимо от `bsp_usb_cdc` и не видят его include paths.
Решение: INTERFACE библиотека `sdk_usb_config` в `sdk/CMakeLists.txt`:
```cmake
add_library(sdk_usb_config INTERFACE)
target_include_directories(sdk_usb_config SYSTEM
INTERFACE ${CMAKE_SOURCE_DIR}/bsp/usb_cdc/src)
```
Все SDK USB таргеты линкуют `sdk_usb_config` и находят конфиг-хедеры при
компиляции. Циклических зависимостей нет — `sdk_usb_config` не содержит кода.
| Код | Условие |
| ------------------- | ------------------------------------------ |
| `BSP_OK` | Transfer поставлен в очередь |
| `BSP_ERR_BUSY` | Предыдущий transfer не завершён |
| `BSP_ERR_NOT_READY` | Хост не подключён |
| `BSP_ERR_INVALID` | `data == NULL`, `len == 0` или `len > 512` |
---
@ -129,152 +91,16 @@ target_include_directories(sdk_usb_config SYSTEM
/* После board_hw_init() + bsp_tick_init(): */
bsp_usb_cdc_init();
/* Ждём подключения хоста */
while (!bsp_usb_cdc_is_ready()) {
/* USB enumeration в процессе */
}
while (!bsp_usb_cdc_is_ready()) { /* ждём enumeration */ }
/* TX — неблокирующая отправка */
const char *msg = "Hello from TFT Board\r\n";
const char *msg = "Hello\r\n";
bsp_usb_cdc_write((const uint8_t *)msg, strlen(msg));
/* RX — polling в main loop */
uint8_t buf[64];
size_t n = bsp_usb_cdc_read(buf, sizeof(buf));
if (n > 0) {
/* обработать buf[0..n-1] */
}
```
---
## API
### `bsp_usb_cdc_init()`
Полная инициализация: USB PHY clock 480 MHz → EHCI0 init → endpoint registration →
NVIC enable → USB_DeviceRun. Включает задержку 5 мс для стабилизации DP pull-down.
**Предусловие**: `board_hw_init()` вызван (MPU настроен, NonCacheable регион активен).
Возвращает `BSP_OK` или `BSP_ERR_HW`.
### `bsp_usb_cdc_is_ready()`
`true` когда USB enumeration завершён **и** хост открыл COM-порт (DTR установлен
через `SET_CONTROL_LINE_STATE`). До этого момента `write()` вернёт `BSP_ERR_NOT_READY`.
### `bsp_usb_cdc_write(data, len)`
Неблокирующая отправка. Копирует данные в NonCacheable TX буфер и ставит в очередь
USB IN transfer. Максимум `BSP_USB_CDC_MAX_PACKET_SIZE` (512) байт за вызов.
| Возврат | Условие |
| ------------------- | ------------------------------------------ |
| `BSP_OK` | Transfer поставлен в очередь |
| `BSP_ERR_BUSY` | Предыдущий transfer не завершён |
| `BSP_ERR_NOT_READY` | Хост не подключён |
| `BSP_ERR_INVALID` | `data == NULL`, `len == 0` или `len > 512` |
Проверить готовность TX канала перед отправкой: `bsp_usb_cdc_write_ready()`.
### `bsp_usb_cdc_write_ready()`
`true` если предыдущий TX transfer завершён и хост подключён.
Удобно для non-blocking write loop:
```c
if (bsp_usb_cdc_write_ready()) {
bsp_usb_cdc_write(data, len);
}
```
### `bsp_usb_cdc_read(buf, max_len)`
Неблокирующее чтение. Забирает данные из RX буфера, заполненного USB OUT ISR callback.
Автоматически перепланирует следующий OUT transfer. Возвращает количество прочитанных
байт (0 если данных нет).
```c
/* Polling в main loop: */
uint8_t buf[64];
size_t n = bsp_usb_cdc_read(buf, sizeof(buf));
```
### `bsp_usb_cdc_poll()`
Зарезервировано. Для bare-metal на EHCI NXP стек обрабатывает всё в ISR.
Для будущего использования с `USB_DEVICE_CONFIG_USE_TASK`.
---
## NonCacheable память
USB EHCI DMA требует некэшируемые буферы. Модуль размещает буферы через макросы
`USB_DMA_INIT_DATA_ALIGN()` и `USB_DMA_NONINIT_DATA_ALIGN()`, которые помещают
данные в секции `NonCacheable.init` и `NonCacheable`.
Линкер-скрипт размещает эти секции в OCRAM (`m_data2`, `0x20200000`).
`board_mpu_init()` настраивает MPU region 9 для этой области.
Проверка: `firmware_test/main.c` содержит `ncache_test_run()` — верификация
что NonCacheable буфер физически попадает в ожидаемый регион.
**Объём**: ~2.5 KB (два bulk буфера по 512 байт + дескрипторы + ACM info +
setup buffer). При NonCacheable регионе 8 KB запас достаточный.
---
## ISR и синхронизация
```bash
USB_OTG1_IRQHandler (usb_cdc_hw.c)
└── USB_DeviceEhciIsrFunction() (SDK)
├── BulkOut callback → s_recvSize = len (volatile)
├── BulkIn callback → s_txIdle = 1 (volatile)
└── DeviceCallback → s_cdcState.attach (volatile)
```
Синхронизация между ISR и main loop:
- **RX**: `bsp_usb_cdc_read()` входит в critical section (`DisableGlobalIRQ`),
копирует `s_recvSize`, сбрасывает в 0, выходит. Копирование из `s_recvBuf`
происходит после выхода из critical section.
- **TX**: `s_txIdle` — volatile flag, устанавливается в BulkIn callback (ISR),
проверяется в `bsp_usb_cdc_write()` (main loop). Гонка исключена: write
сбрасывает flag перед `USB_DeviceSendRequest`.
---
## FreeRTOS
Модуль работает без изменений в контексте FreeRTOS-задачи:
| Контекст | TX | RX |
| ---------- | ------------------------------------------- | ------------------------------ |
| bare-metal | `bsp_usb_cdc_write()` — non-blocking | `bsp_usb_cdc_read()` — polling |
| FreeRTOS | Из задачи, `write_ready()` + `vTaskDelay()` | Из задачи с yield |
Для минимальной латентности в FreeRTOS — будущий `USB_DEVICE_CONFIG_USE_TASK=1`
с `bsp_usb_cdc_poll()` из выделенной задачи.
`USB_DEVICE_INTERRUPT_PRIORITY` (3) должен быть ниже
`configMAX_SYSCALL_INTERRUPT_PRIORITY` при использовании FreeRTOS API из ISR.
---
## Подключение
```cmake
# bsp/CMakeLists.txt — уже добавлено
add_subdirectory(usb_cdc)
# firmware/test/CMakeLists.txt
target_link_libraries(firmware_test PRIVATE
bsp_board
bsp_tick
bsp_usb_cdc
)
if (n > 0) { /* обработать buf[0..n-1] */ }
```
---
@ -283,70 +109,46 @@ target_link_libraries(firmware_test PRIVATE
### HIL-тест
USB CDC появляется как второй COM-порт на хосте (помимо MCU-Link VCOM).
C-прошивка `tests/target/hil_usb_cdc/` — CLI через USB CDC.
pytest: `tools/hil/test_usb_cdc.py` — отправка/приём через pyserial.
Переменная окружения `HIL_USB_CDC_PORT` — порт USB CDC устройства таргета.
USB CDC появляется как второй COM-порт (помимо MCU-Link VCOM).
C-прошивка: `tests/target/hil_usb_cdc/` — CLI через USB CDC.
pytest: `tools/hil/05_test_usb_cdc.py` — через pyserial (`HIL_USB_CDC_PORT`).
```bash
just host::hil-usb-cdc
```
Команды CLI прошивки:
| Команда | Ответ | Описание |
| ------------- | -------- | --------------- |
| `PING` | `PONG` | Проверка канала |
| `ECHO <data>` | `<data>` | Echo-back |
| Команда | Ответ | Описание |
| ------------- | -------- | ---------------- |
| `PING` | `PONG` | Проверка канала |
| `ECHO <data>` | `<data>` | Echo-back данных |
### Host unit-тесты
Не применяются — модуль полностью завязан на USB hardware и NXP middleware.
Тестирование только через HIL.
Host unit-тесты не применяются — модуль полностью завязан на USB hardware.
---
## Конфигурация
## Интеграция
Все настройки находятся в приватных хедерах `src/`:
| Контекст | TX | RX |
| ---------- | -------------------------------------------- | ------------------------------ |
| bare-metal | `write()` — non-blocking | `read()` — polling в main loop |
| FreeRTOS | `write_ready()` + `write()` + `vTaskDelay()` | `read()` из задачи с yield |
| Файл | Настройка | Значение | Описание |
| ------------------------- | ------------------------------- | -------- | -------------------------------- |
| `usb_device_config.h` | `USB_DEVICE_CONFIG_EHCI` | `1` | Контроллер EHCI0 |
| `usb_device_config.h` | `USB_DEVICE_CONFIG_ENDPOINTS` | `4` | EP0 + interrupt IN + bulk IN/OUT |
| `usb_device_config.h` | `USB_DEVICE_CONFIG_SELF_POWER` | `1` | Self-powered device |
| `usb_device_descriptor.h` | `USB_DEVICE_VID` | `0x1234` | Vendor ID (placeholder) |
| `usb_device_descriptor.h` | `USB_DEVICE_PID` | `0x0001` | Product ID (placeholder) |
| `usb_cdc_hw.c` | `USB_DEVICE_INTERRUPT_PRIORITY` | `3` | NVIC приоритет |
| `usb_cdc_hw.c` | `BOARD_USB_PHY_D_CAL` | `0x0C` | PHY калибровка |
`USB_DEVICE_INTERRUPT_PRIORITY = 3` должен быть ниже
`configMAX_SYSCALL_INTERRUPT_PRIORITY` при использовании FreeRTOS API из ISR.
---
## Файловая структура
## CMake
```bash
bsp/usb_cdc/
├── CMakeLists.txt
├── README.md
├── include/
│ └── bsp/
│ └── usb_cdc.h # публичный API — без NXP хедеров
└── src/
├── usb_cdc.c # BSP API + USB device callbacks
├── usb_cdc_descriptors.c # дескрипторы + descriptor callbacks
├── usb_cdc_hw.c # clock, PHY init, IRQ handler
├── usb_device_ch9.c # lite Chapter 9 (копия из NXP примера)
├── usb_device_ch9.h # lite ch9 API
├── usb_device_cdc_acm.h # lite: только CDC request codes
├── usb_device_config.h # конфигурация USB стека
├── usb_device_descriptor.h # VID/PID, endpoints, packet sizes
└── fsl_os_abstraction_config.h # OSA bare-metal конфиг
```cmake
target_link_libraries(firmware_test PRIVATE
bsp_board
bsp_tick
bsp_usb_cdc
)
```
---
## Зависимости
**Зависимости модуля:**
| Зависимость | Тип | Описание |
| --------------------- | --------------------- | ------------------------------------------------------- |
@ -354,19 +156,5 @@ bsp/usb_cdc/
| `bsp_board` | PRIVATE | Транзитивно: `clock_config.h`, `pin_mux.h`, SDK headers |
| `sdk_usb_device_ehci` | PRIVATE | EHCI контроллер + DCI абстракция |
| `sdk_usb_phy` | PRIVATE | USB PHY инициализация (480 MHz PLL) |
| `sdk_osa_bm` | PRIVATE | OS Abstraction Layer (bare-metal, generic list) |
| `sdk_usb_common` | PRIVATE (транзитивно) | USB common headers (`usb.h`, `usb_misc.h`) |
| `sdk_osa_bm` | PRIVATE | OS Abstraction Layer (bare-metal) |
| `sdk_usb_config` | PRIVATE (транзитивно) | INTERFACE: проброс конфиг-хедеров в SDK |
### Зависимости на уровне SDK CMake
```bash
sdk_usb_device_ehci ─┬─ sdk_usb_common ── sdk_osa_bm ── sdk_usb_config
└─ sdk_usb_config │ │
components/osa bsp/usb_cdc/src/
sdk_usb_phy ── sdk_usb_common components/lists (конфиг-хедеры)
```
`sdk_usb_config` — INTERFACE библиотека без кода. Единственная роль —
прокинуть include path к `bsp/usb_cdc/src/` для SDK таргетов,
которым нужны `usb_device_config.h` и `fsl_os_abstraction_config.h`.

View file

@ -22,43 +22,50 @@ HIL-тесты через pyOCD + pytest, GDB-сервер для отладки
## 2. Компоненты окружения
```bash
ПК разработчика
├── Хост (Linux / macOS / Windows + Git Bash)
│ ├── just ← запуск задач хостового уровня (just host::*)
│ ├── docker ← управление devcontainer
│ ├── uv + spsdk ← прошивка через USB ROM (flash_usb.py, sdphost, blhost)
│ │ venv: tools/host/
│ ├── uv + pyocd ← GDB-сервер отладки + HIL-тесты
│ │ + pyserial venv: tools/hil/
│ │ + pytest
│ │ + mpremote ← деплой агента на M5StampPLC
│ └── VSCode ← IDE (Dev Containers extension)
├── Devcontainer (Docker)
│ ├── arm-none-eabi-gcc ← кросс-компилятор (firmware + HIL target-прошивки)
│ ├── cmake + ninja ← система сборки
│ ├── clang-17 ← компилятор для host-тестов
│ ├── clangd-17 ← LSP (автодополнение, диагностика)
│ ├── clang-tidy-17 ← статический анализ
│ ├── clang-format-17 ← форматирование кода
│ ├── just ← запуск задач внутри контейнера (just build::*)
│ ├── uv + spsdk ← сборка HAB-образов (только nxpimage)
│ └── Unity + fff ← фреймворки host-тестов
├── Плата TFT (MIMXRT1052)
│ ├── USB ──────────────────────▶ хост (SDP-режим, прошивка через ROM)
│ └── MCU-Link (USB) ───────────▶ хост (CMSIS-DAP)
│ ├── SWD ← pyOCD: GDB-сервер отладки + прошивка Flash + загрузка HIL ELF в RAM
│ └── VCOM ← pytest общается с HIL прошивкой через UART CLI
└── HIL стенд (M5Stack StamPLC)
├── USB ──────────────────────▶ хост (M5 агент, JSON-lines CLI)
├── RLY1 ─────────────────────▶ VIN таргета (управление питанием)
├── RLY2 ─────────────────────▶ RS_RX таргета (BSP_OPTO_CH_RS)
├── RLY3 ─────────────────────▶ EXT_IN1 таргета (BSP_OPTO_CH_IN1)
└── RLY4 ─────────────────────▶ EXT_IN2 таргета (BSP_OPTO_CH_IN2)
### Физические связи
```mermaid
graph LR
Host["Хост"]
subgraph Board["Плата TFT (MIMXRT1052)"]
USB_SDP["USB"]
MCULink["MCU-Link"]
end
subgraph M5["HIL стенд (M5StampPLC)"]
M5_USB["USB"]
RLY["RLY14"]
end
USB_SDP -->|"SDP — прошивка через ROM"| Host
MCULink -->|"SWD — GDB-сервер, прошивка Flash, загрузка HIL ELF"| Host
MCULink -->|"VCOM — UART CLI (pytest ↔ HIL firmware)"| Host
M5_USB -->|"JSON-lines CLI"| Host
RLY -->|"VIN · RS_RX · EXT_IN1 · EXT_IN2"| Board
```
### Состав инструментов
```mermaid
graph TB
subgraph Host["Хост (Linux / macOS / Windows + Git Bash)"]
H1["just host::*\nзапуск задач хостового уровня"]
H2["docker\nуправление devcontainer"]
H3["uv + spsdk — tools/host/\nsdphost · blhost · nxpimage"]
H4["uv + pyocd + pyserial + pytest — tools/hil/\nGDB-сервер · HIL-тесты"]
H5["mpremote\nдеплой агента на M5StampPLC"]
H6["VSCode (Dev Containers extension)"]
end
subgraph DC["Devcontainer (Docker)"]
D1["arm-none-eabi-gcc\nкросс-компилятор firmware + HIL"]
D2["cmake + ninja\nсистема сборки"]
D3["clang-17 · clangd-17\nclang-tidy · clang-format"]
D4["uv + spsdk — tools/host/\nтолько nxpimage (HAB-образы)"]
D5["Unity + fff\nфреймворки host-тестов"]
D6["just build::*\nзапуск задач внутри контейнера"]
end
```
---
@ -130,17 +137,29 @@ HIL_USB_CDC_TIMEOUT=5.0
**Как значения попадают в инструменты:**
```bash
.env
├─▶ just (set dotenv-load + set export)
│ ├─▶ just-рецепты: {{BOOTROM_VID}}, {{HIL_VCOM_PORT}}, {{GDB_PORT}}
│ └─▶ uv run python ← наследует os.environ автоматически
│ ├─▶ flash_usb.py: os.environ["BOOTROM_VID"]
│ ├─▶ flash_swd.py: os.environ["PYOCD_TARGET"]
│ └─▶ env_config.py: os.environ["HIL_VCOM_PORT"]
└─▶ .vscode/launch.json ← через ${env:GDB_PORT}
```mermaid
flowchart LR
ENV[".env"]
subgraph Just["just (dotenv-load + export)"]
JR["just-рецепты\n{{BOOTROM_VID}}\n{{HIL_VCOM_PORT}}\n{{GDB_PORT}}"]
UV["uv run python\n(наследует os.environ)"]
end
subgraph Python["Python-скрипты"]
FU["flash_usb.py\nos.environ[BOOTROM_VID]"]
FS["flash_swd.py\nos.environ[PYOCD_TARGET]"]
EC["env_config.py\nos.environ[HIL_VCOM_PORT]"]
end
VS[".vscode/launch.json\n${env:GDB_PORT}"]
ENV --> Just
JR --> Python
UV --> FU
UV --> FS
UV --> EC
ENV --> VS
```
---
@ -227,7 +246,7 @@ HIL_USB_CDC_TIMEOUT=5.0
└── testing/
├── hil/
│ ├── HIL_HOW_TO.md ← как проводить HIL-тесты
│ ├── HIL_BENCH.md ← стенд: оборудование, подключение
│ ├── HIL_BENCH.md ← стенд: оборудование, подключение
│ └── HIL_CREATE_TEST.md ← как добавить новый HIL-тест
└── host/
└── HOST_CREATE_TEST.md ← как добавить host unit-тест
@ -254,21 +273,23 @@ git clone <repo-url> && cd <repo>
### 5.3 Что делает bootstrap
```bash
bootstrap.sh (уровень 0)
├── определить платформу (Linux / macOS / Windows Git Bash)
├── проверить/установить uv >= 0.4.0
├── проверить/установить just >= 1.36.0 (через uv tool)
└── exec just host::bootstrap
├── [1/3] check-deps — just · uv · docker
├── [2/3] setup-udev — udev-правила NXP USB (только Linux)
│ 1FC9:0130 ← BootROM SDP
│ 15A2:0073 ← Flashloader
│ dialout ← группа для /dev/ttyACM* (M5StampPLC)
└── [3/3] setup-tools — uv sync в tools/host/
SHA-256 uv.lock кешируется → повторный вызов мгновенный
```mermaid
flowchart TD
A["bootstrap.sh\n(уровень 0)"]
A --> B["определить платформу\nLinux / macOS / Windows Git Bash"]
B --> C["проверить/установить\nuv >= 0.4.0"]
C --> D["проверить/установить\njust >= 1.36.0\n(через uv tool)"]
D --> E["exec just host::bootstrap"]
E --> F["[1/3] check-deps\njust · uv · docker"]
E --> G["[2/3] setup-udev\n(только Linux)"]
E --> H["[3/3] setup-tools\nuv sync → tools/host/"]
G --> G1["1FC9:0130 — BootROM SDP"]
G --> G2["15A2:0073 — Flashloader"]
G --> G3["dialout — /dev/ttyACM*"]
H --> H1["SHA-256 uv.lock кешируется\nповторный вызов мгновенный"]
```
### 5.4 После bootstrap
@ -323,7 +344,7 @@ buildPresets (HIL):
target-debug-build: test_host_uart, test_hil_button, test_hil_can,
test_hil_usb_cdc, test_hil_opto
Источник истины по списку целей — [CMakePresets.json](../CMakePresets.json).
Источник истины по списку целей — CMakePresets.json.
```
### 6.3 Boot-стратегии
@ -431,22 +452,36 @@ HIL-тесты проверяют периферию на реальном же
Только MCU-Link: SWD загружает ELF в RAM, VCOM обеспечивает UART CLI.
```bash
pytest → uart_cmd("PING")
↓ pyserial / VCOM
MCU-Link
↓ LPUART1
RT1052 → "PONG"
```mermaid
sequenceDiagram
participant PT as pytest
participant ML as MCU-Link VCOM
participant RT as RT1052
PT->>ML: uart_cmd("PING")\n(pyserial)
ML->>RT: LPUART1
RT-->>ML: "PONG"
ML-->>PT: "PONG"
```
### С M5StampPLC — `02_test_opto.py` и другие
`M5StampPLC` управляет входными сигналами таргета через реле. pytest оркестрирует оба канала одновременно.
```bash
pytest
├─▶ m5.opto_set(1, True) → M5 (JSON) → RLY3 → EXT_IN1 таргета
└─▶ uart_cmd("OPTO_READ 1") → MCU-Link VCOM → RT1052 → "ACTIVE"
```mermaid
sequenceDiagram
participant PT as pytest
participant M5 as M5StampPLC
participant ML as MCU-Link VCOM
participant RT as RT1052
PT->>M5: m5.opto_set(1, True)\n(JSON-lines)
M5->>RT: RLY3 → EXT_IN1
PT->>ML: uart_cmd("OPTO_READ 1")\n(pyserial)
ML->>RT: LPUART1
RT-->>ML: "ACTIVE"
ML-->>PT: "ACTIVE"
```
Перед каждой тест-сессией фикстура `m5` автоматически включает питание таргета (RLY1), ждёт стабилизации, затем `loaded_<n>` загружает ELF через pyOCD.
@ -503,17 +538,26 @@ timeout-паттерн.
Подробно — [docs/HOW_TO_DEBUG.md](HOW_TO_DEBUG.md). Краткая схема:
```bash
Хост
├── just host::debug-server
│ └── pyocd gdbserver :3333
│ USB/SWD → MCU-Link → плата
└── host.docker.internal:3333 ← доступен из devcontainer
```mermaid
flowchart LR
subgraph DC["Devcontainer"]
CD["cortex-debug\n(VSCode F5)"]
GDB["arm-none-eabi-gdb"]
CD --> GDB
end
Devcontainer
└── cortex-debug (VSCode)
↔ arm-none-eabi-gdb
target remote host.docker.internal:3333
subgraph Host["Хост"]
DS["just host::debug-server"]
PO["pyocd gdbserver :3333"]
DS --> PO
end
ML["MCU-Link\n(USB/SWD)"]
Board["Плата TFT"]
GDB -->|"TCP host.docker.internal:3333"| PO
PO --> ML
ML -->|"SWD"| Board
```
Три конфигурации в `.vscode/launch.json`:
@ -529,28 +573,41 @@ RTT-логи доступны в Debug-сборках (`SEGGER_RTT_ENABLED=ON`);
## 12. Жизненный цикл изменений
```bash
feature-ветка
├── devcontainer
│ just build::test-host ← зелёные host-тесты?
│ just build::build-firmware-test-debug
│ just build::build-hil ← HIL-прошивки собираются?
├── хост
│ just host::flash-test-debug ← прошить, проверить на железе
│ just host::hil-run ← HIL зелёные?
├── подготовка к MR
│ just build::hab-all-release
│ just host::flash firmware_test release
└── Merge Request → CI
host-тесты · сборка · HIL · публикация артефактов
Производственный сервер
just host::incoming → firmware_test release → HIL
just host::production → bootloader + tft_app release
```mermaid
flowchart TD
FB["feature-ветка"]
subgraph DC["Devcontainer"]
T1["just build::test-host\nhost-тесты зелёные?"]
T2["just build::build-firmware-test-debug"]
T3["just build::build-hil\nHIL-прошивки собираются?"]
end
subgraph HostW["Хост"]
T4["just host::flash-test-debug\nпрошить, проверить на железе"]
T5["just host::hil-run\nHIL зелёные?"]
end
subgraph MR["Подготовка к MR"]
T6["just build::hab-all-release"]
T7["just host::flash firmware_test release"]
end
CI["Merge Request → CI\nhost-тесты · сборка · HIL\nпубликация артефактов"]
subgraph Prod["Производственный сервер"]
P1["just host::incoming\nfirmware_test release → HIL"]
P2["just host::production\nbootloader + tft_app release"]
end
FB --> DC
T1 --> T2 --> T3
DC --> HostW
T4 --> T5
HostW --> MR
T6 --> T7
MR --> CI
CI --> Prod
```
---

View file

@ -2,23 +2,33 @@
## Обзор архитектуры
Отладка построена на проброске GDB-сервера с хоста в devcontainer по TCP. Это позволяет держать весь инструментарий сборки и языковой сервер внутри контейнера, не проводя USB-пробник внутрь Docker.
Отладка построена на пробросе GDB-сервера с хоста в devcontainer по TCP. Это
позволяет держать весь инструментарий сборки и языковой сервер внутри контейнера,
не проводя USB-пробник внутрь Docker.
```bash
┌─────────────────────────────────────┐ ┌──────────────────────────────────┐
│ Хост (macOS/Linux) │ │ DevContainer │
│ │ │ │
│ just host::debug-server │ │ VSCode + cortex-debug │
│ └─ pyocd gdbserver :3333 ──────────┼─────┼──► arm-none-eabi-gdb │
│ │TCP │ └─ символы из .elf │
│ MCU-Link (CMSIS-DAP) │3333 │ │
│ └─ SWD ──► MIMXRT1052 │ │ RTT Console (SEGGER RTT логи) │
│ Flash / SDRAM │ │ Peripherals (SVD регистры) │
│ SEGGER RTT буфер │ │ RTOS view (FreeRTOS задачи) │
└─────────────────────────────────────┘ └──────────────────────────────────┘
```mermaid
flowchart LR
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
```
**Ключевой принцип:** `pyocd gdbserver` запускается на хосте и слушает на `0.0.0.0:3333`. Из контейнера GDB подключается через `host.docker.internal:3333` — специальный DNS-алиас Docker, который резолвится в IP хост-машины.
**Ключевой принцип:** `pyocd gdbserver` слушает на `0.0.0.0:3333`. Из контейнера
GDB подключается через `host.docker.internal:3333` — специальный DNS-алиас Docker,
резолвится в IP хост-машины.
---
@ -48,10 +58,9 @@
### Конфигурация
Параметры отладки задаются в `.env` и автоматически экспортируются через `just` (`set export`), откуда наследуются скриптами:
Параметры отладки задаются в `.env`:
```bash
# .env — секция Debug / SWD
GDB_PORT=3333
PYOCD_TARGET=mimxrt1050_quadspi
PYOCD_FREQUENCY=4000000
@ -60,7 +69,7 @@ FCB_PATH=tools/host/dcd/w25q128_fdcb.bin
---
## Прошивки, поддерживаемые отладкой
## Поддерживаемые прошивки
| Конфигурация VSCode | ELF | Особенности |
| ----------------------------- | ------------------------------- | ---------------------------- |
@ -68,7 +77,7 @@ FCB_PATH=tools/host/dcd/w25q128_fdcb.bin
| `🐛 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`).
Все три — XIP-прошивки, исполняются из QuadSPI NOR Flash (`0x60000000`).
---
@ -76,32 +85,29 @@ FCB_PATH=tools/host/dcd/w25q128_fdcb.bin
### Режим А — прошивка уже в Flash
Стандартный ежедневный сценарий. Прошивка была залита ранее любым способом и исполняется на плате.
```bash
# 1. Хост — запустить GDB-сервер (оставить работать в отдельном терминале)
# 1. Хост — запустить GDB-сервер (оставить в отдельном терминале)
just host::debug-server
# 2. DevContainer — VSCode
# Run & Debug (Ctrl+Shift+D) → выбрать конфигурацию → F5
```
GDB сбрасывает MCU, загружает символы из ELF и останавливается на входе в `main`. Flash не перезаписывается.
GDB сбрасывает MCU, загружает символы из ELF и останавливается на входе
в `main`. Flash не перезаписывается.
### Режим Б — прошить через SWD, затем отладить
Когда нужно обновить прошивку без перевода платы в режим Serial Downloader. Удобно при итеративной разработке когда плата закреплена в стенде.
```bash
# 1. DevContainer — собрать HAB-образ
# 1. DevContainer
just build::hab-firmware-test-debug
# 2. Хост — прошить через SWD (MCU-Link, без смены BOOT_MODE)
# 2. Хост
just host::flash-swd-test-debug
# 3. ⚡ Power cycle платы (обязательно — VECTRESET не реинициализирует FlexSPI)
# 3. ⚡ Power cycle платы (обязательно)
# 4. Хост — запустить GDB-сервер
# 4. Хост
just host::debug-server
# 5. DevContainer — VSCode → 🐛 Debug: firmware_test → F5
@ -109,16 +115,14 @@ just host::debug-server
### Режим В — прошить через USB SDP, затем отладить
Классический способ. Требует перевода платы в режим Serial Downloader (BOOT_MODE = 01).
```bash
# 1. DevContainer — собрать
# 1. DevContainer
just build::build-firmware-test-debug
# 2. Хост — перевести плату в Serial Downloader mode, затем:
# 2. Хост — перевести плату в SDP-режим, затем:
just host::flash-test-debug
# 3. Хост — запустить GDB-сервер
# 3. Хост
just host::debug-server
# 4. DevContainer — VSCode → 🐛 Debug: firmware_test → F5
@ -128,33 +132,32 @@ just host::debug-server
## Почему flash через SWD требует FCB
При прошивке через USB SDP (режимы А и В) ROM-загрузчик сам инициализирует FlexSPI контроллер по DCD из HAB-образа — Flash Configuration Block ему не нужен.
При USB SDP ROM-загрузчик инициализирует FlexSPI по DCD из HAB-образа — FCB
не нужен. При SWD flash-алгоритм pyOCD пишет в NOR Flash напрямую. При
cold-start Boot ROM сначала читает FCB по адресу `0x60000000`, конфигурирует
FlexSPI, и только потом ищет IVT. Без FCB бутлоадер не стартует.
При прошивке через SWD flash-алгоритм pyOCD записывает данные напрямую в NOR Flash. При cold-start Boot ROM первым делом читает FCB по адресу `0x60000000`, конфигурирует по нему FlexSPI, и только потом ищет IVT. Без FCB бутлоадер не может обратиться к Flash.
`flash_swd.py` решает это, собирая итоговый образ перед записью:
`flash_swd.py` решает это, собирая образ перед записью:
```bash
0x60000000 w25q128_fdcb.bin (512 байт) — FCB: параметры W25Q128, Quad SPI
0x60000200 0xFF × 3584 байт — padding (значение стёртой ячейки)
0x60001000 firmware_test_hab.bin — IVT + DCD + код (ivtOffset = 0x1000)
0x60000000 w25q128_fdcb.bin (512 байт) — FCB
0x60000200 0xFF × 3584 байт — padding
0x60001000 firmware_test_hab.bin — IVT + DCD + код
```
Весь диапазон `0x600000000x6000FFFF` умещается в один 64KB-сектор Flash, поэтому стирается и записывается за одну транзакцию — FCB и HAB не перезаписывают друг друга.
Весь диапазон `0x600000000x6000FFFF` — один 64KB сектор: стирается и
записывается за одну транзакцию.
---
## RTT-логи
SEGGER RTT включён только в Debug-сборках (`SEGGER_RTT_ENABLED=ON` в `CMakePresets.json`). В Release-сборках RTT отключён и символ `_SEGGER_RTT` в ELF отсутствует.
После старта отладки вкладка `TERMINAL → RTT` в VSCode принимает вывод из RTT-буфера канала 0. `cortex-debug` находит адрес буфера автоматически по символу `_SEGGER_RTT` из ELF (`address: auto` в `launch.json`).
Использование в коде:
SEGGER RTT включён только в Debug-сборках (`SEGGER_RTT_ENABLED=ON`).
После старта отладки вкладка `TERMINAL → RTT` принимает вывод канала 0.
`cortex-debug` находит адрес буфера по символу `_SEGGER_RTT` из ELF.
```c
#include "SEGGER_RTT.h"
SEGGER_RTT_printf(0, "value = %d\n", value);
```
@ -162,46 +165,47 @@ SEGGER_RTT_printf(0, "value = %d\n", value);
## FreeRTOS task view
Конфигурация `🐛 Debug: tft_app (FreeRTOS)` включает `"rtos": "FreeRTOS"` — cortex-debug разбирает внутренние структуры планировщика и показывает вкладку `RTOS` с таблицей задач: имя, состояние (`Running` / `Ready` / `Blocked` / `Suspended`), использование стека, приоритет. При паузе можно переключиться в контекст любой задачи и просмотреть её стек вызовов.
Конфигурация `🐛 Debug: tft_app (FreeRTOS)` включает `"rtos": "FreeRTOS"`
cortex-debug разбирает структуры планировщика и показывает вкладку `RTOS`
с таблицей задач: имя, состояние, использование стека, приоритет.
---
## Просмотр регистров периферии
Вкладка `Peripherals` в панели отладки показывает все периферийные блоки MIMXRT1052 по SVD-файлу `bsp/generated/startup/MIMXRT1052.xml`. Значения регистров обновляются при каждой паузе. Можно раскрыть любой блок (GPIO, LPUART, USB, FlexSPI и т.д.) и просматривать поля побитово.
Вкладка `Peripherals` показывает все блоки MIMXRT1052 по SVD-файлу
`bsp/generated/startup/MIMXRT1052.xml`. Значения обновляются при каждой паузе.
---
## Ограничения и важные замечания
## Ограничения
**MCU-Link монопольный ресурс.** `debug-server` и `flash-swd` не могут работать одновременно — оба занимают пробник. Перед `flash-swd` остановите сервер (Ctrl+C), и наоборот.
**MCU-Link монопольный ресурс.** `debug-server` и `flash-swd` не могут
работать одновременно. Перед `flash-swd` остановите сервер (Ctrl+C).
**HIL-тесты vs отладка.** pyOCD также используется для HIL (загрузка ELF в RAM через `pyocd.yaml`). Перед запуском HIL-тестов (`just host::hil-run`) остановите GDB-сервер.
**HIL-тесты vs отладка.** pyOCD также используется для HIL. Перед
`just host::hil-run` остановите GDB-сервер.
**Power cycle после flash-swd обязателен.** pyOCD завершает запись командой VECTRESET, которая не реинициализирует FlexSPI контроллер. Boot ROM при таком сбросе не может прочитать FCB и не стартует из Flash. Только полное отключение питания гарантирует корректный cold-start.
**Power cycle после flash-swd обязателен.** VECTRESET не реинициализирует
FlexSPI — только полное отключение питания гарантирует корректный cold-start.
**Только Debug-сборки.** Отладка с символами возможна только для `Debug` CMake-пресета. Release-сборки компилируются с `-O2` без DWARF-символов.
**Только Debug-сборки.** Release компилируется с `-O2` без DWARF-символов.
---
## Быстрый старт (первый запуск)
```bash
# 1. Убедиться что cortex-debug установлен в devcontainer
# .devcontainer/devcontainer.json → extensions: ["marus25.cortex-debug"]
# 2. Убедиться что в .devcontainer/devcontainer.json есть (для Linux-хостов):
# 1. Убедиться что в .devcontainer/devcontainer.json есть (для Linux):
# "runArgs": ["--add-host=host.docker.internal:host-gateway"]
# 3. Залить прошивку любым способом (один раз)
just host::flash-test-debug # USB SDP
# или
just host::flash-swd-test-debug # SWD (после just build::hab-firmware-test-debug)
# 2. Залить прошивку
just host::flash-test-debug
# 4. Запустить GDB-сервер на хосте
# 3. Хост — запустить GDB-сервер
just host::debug-server
# 5. В VSCode (devcontainer)
# 4. DevContainer — VSCode
# Ctrl+Shift+D → 🐛 Debug: firmware_test → F5
```
@ -213,7 +217,7 @@ just host::debug-server
.
├── .env # GDB_PORT, PYOCD_TARGET, PYOCD_FREQUENCY, FCB_PATH
├── .vscode/
│ ├── launch.json # Конфигурации cortex-debug (3 проекта)
│ ├── launch.json # cortex-debug конфигурации (3 проекта)
│ └── tasks.json # preLaunchTask: build:*-debug
├── bsp/generated/startup/
│ └── MIMXRT1052.xml # SVD — регистры периферии

View file

@ -11,7 +11,9 @@
## Способ 1 — USB SDP (Serial Download Protocol)
Стандартный производственный способ. ROM-загрузчик принимает образ по USB и записывает его во Flash через Flashloader. Требует физического переключения пина `BOOT_MOD_1`.
Стандартный производственный способ. ROM-загрузчик принимает образ по USB и
записывает его во Flash через Flashloader. Требует физического переключения
пина `BOOT_MOD_1`.
### 1.1 Перевести плату в SDP-режим
@ -58,17 +60,15 @@ just host::flash-production # bootloader release + app release (с подт
### 1.4 Что происходит при прошивке через USB SDP
```bash
Плата в SDP-режиме (1FC9:0130)
├── sdphost: загрузить ivt_flashloader.bin в RAM (0x20001C00)
└── sdphost: jump-address → Flashloader поднимается как 15A2:0073
├── configure-memory (0xC0000007) — инициализация FlexSPI NOR
├── flash-erase-region 0x60000000
├── configure-memory (0xF000000F) — запись FCB в 0x60000000
├── write-memory 0x60001000 ← HAB-образ
└── reset
```mermaid
flowchart TD
A["Плата в SDP-режиме\n1FC9:0130"] --> B["sdphost\nзагрузить ivt_flashloader.bin\nв RAM 0x20001C00"]
B --> C["sdphost jump-address\nFlashloader поднимается\nкак 15A2:0073"]
C --> D["configure-memory 0xC0000007\nинициализация FlexSPI NOR"]
D --> E["flash-erase-region 0x60000000"]
E --> F["configure-memory 0xF000000F\nзапись FCB в 0x60000000"]
F --> G["write-memory 0x60001000\nHAB-образ"]
G --> H["reset"]
```
ROM-загрузчик сам конфигурирует FlexSPI через DCD из HAB-образа, поэтому FCB
@ -80,13 +80,14 @@ ROM-загрузчик сам конфигурирует FlexSPI через DCD
Прошивка через отладочный пробник (MCU-Link, CMSIS-DAP). Плата остаётся
в нормальном режиме загрузки — переключать `BOOT_MOD_1` не нужно. Удобно
при итеративной разработке когда плата закреплена в стенде, а также как
часть отладочного цикла.
при итеративной разработке когда плата закреплена в стенде.
**Ограничения:**
- После записи обязателен **power cycle** (не reset) — VECTRESET не реинициализирует FlexSPI, Boot ROM не стартует
- MCU-Link используется монопольно: нельзя запускать одновременно с `debug-server` или HIL-тестами
- После записи обязателен **power cycle** (не reset) — VECTRESET не
реинициализирует FlexSPI, Boot ROM не стартует
- MCU-Link используется монопольно: нельзя запускать одновременно с
`debug-server` или HIL-тестами
### 2.1 Подготовить HAB-образ (внутри devcontainer)
@ -99,8 +100,8 @@ just build::hab-app-debug
### 2.2 Прошить (хостовый терминал)
```bash
just host::flash-swd-test-debug # firmware_test Debug
just host::flash-swd-test-release # firmware_test Release
just host::flash-swd-test-debug
just host::flash-swd-test-release
just host::flash-swd-bootloader-debug
just host::flash-swd-bootloader-release
just host::flash-swd-app-debug
@ -134,7 +135,7 @@ just host::flash-swd-app-release
| `tools/hil/` (uv-проект) | pyocd, вызывается через `uv run` |
FCB-бинарник (`w25q128_fdcb.bin`) генерируется в NXP SecureProvisioningTool
для W25Q128 в режиме Quad SPI и хранится в репозитории — пересоздавать не нужно.
и хранится в репозитории — пересоздавать не нужно.
---

View file

@ -15,29 +15,22 @@
инженером через USB CDC ACM (разъём J2). Загружается через BootROM (USB SDP)
без предварительной прошивки загрузчика.
**Стенд:**
```mermaid
flowchart TB
Host["Хост-ПК\nсервисного инженера"]
FW["Плата MIMXRT1052\nfirmware_test"]
Periph["Периферия\nSDRAM · QSPI Flash · uSD\nDisplay · CAN · UART · Opto"]
M5["M5StampPLC\nвнешние сигналы для HIL"]
```bash
[Хост-ПК сервисного инженера]
│ USB CDC ACM (J2)
│ JSON-lines, 1 строка = 1 сообщение
[Плата MIMXRT1052 с firmware_test]
│ GPIO / LPUART / SEMC / FlexSPI / USDHC
[Периферия: SDRAM, QSPI Flash, uSD, Display, CAN, UART, Opto]
[M5StampPLC — управление внешними сигналами для HIL тестов]
Host -->|"USB CDC ACM J2\nJSON-lines, 1 строка = 1 сообщение"| FW
FW -->|"GPIO / LPUART / SEMC\nFlexSPI / USDHC"| Periph
M5 -->|"реле → оптовходы / CAN / UART"| FW
```
**Принцип работы:**
- Вся тест-логика живёт **на таргете** (`test_runner.c`, `tests/*.c`).
- Хост — тонкий клиент: отправляет команды, отображает события, управляет
интерактивными шагами.
- Инженер запускает тесты **атомарно** (один тест за раз) или все подряд
(`run_all`). Последовательность не фиксирована — инженер сам решает,
что проверять.
**Принцип работы:** вся тест-логика живёт на таргете (`test_runner.c`,
`tests/*.c`). Хост — тонкий клиент: отправляет команды, отображает события,
управляет интерактивными шагами. Инженер запускает тесты атомарно или все
подряд (`run_all`).
---
@ -53,113 +46,90 @@
| CR+LF | Принимается (таргет отбрасывает `\r` перед `\n`) |
| Направление | Двунаправленный, half-duplex по логике |
Нет хэндшейка, нет sequence number, нет подтверждений доставки. При потере
строки хост повторяет команду — таргет идемпотентен для `ping` и `run`.
Нет хэндшейка, нет sequence number, нет подтверждений доставки.
---
## Формат сообщений
Все сообщения — JSON-объекты в одну строку (`\n` в конце).
Все сообщения — JSON-объекты в одну строку (`\n` в конце). Поле `"type"`
определяет смысл сообщения:
### Ключевые поля
```bash
Хост → Таргет: "type": "cmd" — команда
"type": "confirm" — ответ оператора на интерактивный шаг
Каждое сообщение содержит поле `"type"`, определяющее его смысл:
```
Хост → Таргет: "type": "cmd" — команда
"type": "confirm" — ответ оператора на интерактивный шаг
Таргет → Хост: "type": "session_start" — таргет готов
"type": "pong" — ответ на ping
"type": "test_begin" — тест стартовал
"type": "test_result" — тест завершён
"type": "confirm_request" — ожидание действия оператора
"type": "summary" — итог run_all
"ok": false, "error": "…" — ошибка протокола
Таргет → Хост: "type": "session_start" — таргет готов
"type": "pong" — ответ на ping
"type": "test_begin" — тест стартовал
"type": "progress" — промежуточный шаг теста
"type": "test_result" — тест завершён
"type": "confirm_request" — ожидание действия оператора
"type": "summary" — итог run_all
"ok": false, "error": "…" — ошибка протокола
```
---
## Жизненный цикл сессии
```bash
Хост Таргет
│ │
│ [прошивка загружена через USB SDP] │
│ [USB CDC установлен] │
│◄─── {"type":"session_start","fw":"0.1.0", │
│ "target":"IMXRT1052","uptime_ms":0} │
│ │
│──── {"type":"cmd","cmd":"ping"} ──────────►│
│◄─── {"type":"pong"} ────────────────────── │
│ │
│ [инженер выбирает тест] │
│ │
│──── {"type":"cmd","cmd":"run","id":"sdram"} ►│
│◄─── {"type":"test_begin","id":"sdram",...} │
│◄─── {"type":"test_result","id":"sdram",...} │
│ │
│──── {"type":"cmd","cmd":"run_all"} ────────►│
│◄─── {"type":"test_begin","id":"sdram",...} │
│◄─── {"type":"test_result","id":"sdram",...} │
│ … (каждый тест в реестре) … │
│◄─── {"type":"summary","overall":"pass",...} │
│ │
```mermaid
sequenceDiagram
participant H as Хост
participant T as Таргет
Note over T: прошивка загружена через USB SDP
T-->>H: {"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
H->>T: {"type":"cmd","cmd":"ping"}
T-->>H: {"type":"pong"}
Note over H: инженер выбирает тест
H->>T: {"type":"cmd","cmd":"run","id":"sdram"}
T-->>H: {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
T-->>H: {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
H->>T: {"type":"cmd","cmd":"run_all"}
T-->>H: {"type":"test_begin","id":"sdram",...}
T-->>H: {"type":"test_result","id":"sdram","status":"pass",...}
Note over T: ...каждый тест в реестре...
T-->>H: {"type":"summary","overall":"pass","passed":7,"failed":0,"skipped":1}
```
`session_start` отправляется **автоматически** при каждом старте таргета,
до получения первой команды. Хост должен быть готов принять его сразу после
открытия CDC порта.
`session_start` отправляется автоматически при каждом старте таргета, до
получения первой команды.
---
## Команды хоста → таргет (`"type":"cmd"`)
## Команды хоста → таргет
### `ping`
Проверка связи. Таргет отвечает немедленно.
```json
→ {"type":"cmd","cmd":"ping"}
← {"type":"pong"}
```
---
### `run` — запуск одного теста
Запустить тест по идентификатору. Если тест требует предварительного
подтверждения оператора (`pre_confirm_prompt` задан), таргет сначала пошлёт
`confirm_request`.
```json
→ {"type":"cmd","cmd":"run","id":"sdram"}
← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
```
Если `id` не найден в реестре:
Если `id` не найден: `← {"ok":false,"error":"UNKNOWN_TEST"}`
```json
← {"ok":false,"error":"UNKNOWN_TEST"}
```
### `run_all` — запуск всех тестов
---
### `run_all` — запуск всех тестов по реестру
Запускает все тест-модули в порядке реестра. Если тест помечен `"critical":true`
и вернул `"status":"fail"` — выполнение прерывается, остальные тесты
получают `"status":"skip"` в итоге (но `summary` всё равно отправляется).
Если тест помечен `"critical":true` и вернул `"fail"` — выполнение
прерывается, остальные получают `"skip"`.
```json
→ {"type":"cmd","cmd":"run_all"}
← {"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","name":"QSPI Flash 8 MB","critical":true}
← {"type":"test_result","id":"qspi","status":"pass","ms":88,"detail":""}
← ... (остальные тесты) ...
← {"type":"test_begin","id":"sdram",...}
← {"type":"test_result","id":"sdram","status":"pass",...}
← ... (каждый тест в реестре) ...
← {"type":"summary","passed":7,"failed":0,"skipped":1,"overall":"pass"}
```
@ -169,70 +139,40 @@
### `session_start`
Таргет готов к работе. Отправляется автоматически при старте.
```json
{
"type": "session_start",
"fw": "0.1.0",
"target": "IMXRT1052",
"uptime_ms": 0
}
{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
```
| Поле | Тип | Описание |
| ----------- | ------ | -------------------------- |
| `fw` | string | Версия firmware_test |
| `target` | string | Идентификатор платформы |
| `uptime_ms` | number | Время с момента старта, мс |
---
### `test_begin`
Тест начат. Отправляется непосредственно перед вызовом `run()`.
```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`
Тест завершён (pass / fail / skip).
```json
{
"type": "test_result",
"id": "sdram",
"status": "pass",
"ms": 312,
"detail": ""
}
{"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
```
| `status` | Смысл |
| -------- | ---------------------------------------------------------------------------- |
| `"pass"` | Тест пройден |
| `"fail"` | Тест провален; поле `detail` содержит описание |
| `"skip"` | Тест пропущен (нет оборудования, таймаут оператора, прерван `critical` fail) |
| `status` | Смысл |
| -------- | ----------------------------------------------------------------------- |
| `"pass"` | Тест пройден |
| `"fail"` | Тест провален; `detail` содержит описание |
| `"skip"` | Пропущен (нет оборудования, таймаут оператора, прерван `critical` fail) |
Поле `detail` — произвольная ASCII-строка до 95 символов. При `pass` — пустая.
Примеры: `"addr=0x80001000 expected=0xA5 got=0x00"`, `"JEDEC ID mismatch"`.
`detail` — ASCII-строка до 95 символов. При `pass` — пустая.
---
### `progress`
```json
{"type":"progress","test":"usd","step":"mount","status":"ok"}
```
Промежуточные шаги внутри теста. Используется в `usd`.
### `confirm_request`
Таргет ожидает действия оператора. Используется интерактивными тестами:
display (подтвердить цвет), кнопки (нажать кнопку), uSD (вставить карту).
```json
{
"type": "confirm_request",
@ -242,58 +182,28 @@ display (подтвердить цвет), кнопки (нажать кнопк
}
```
Хост должен отобразить `prompt` оператору и ждать его реакции. Если оператор
не ответил за `timeout_ms` — таргет переходит в `SKIP` для этого шага
автоматически. Хост может дублировать таймаут на своей стороне для UX, но
авторитетный таймаут — на таргете.
---
Хост отображает `prompt` оператору. Авторитетный таймаут — на таргете;
если оператор не ответил за `timeout_ms` — таргет переходит в `SKIP`.
### `summary`
Итог `run_all`. Отправляется после завершения последнего теста в реестре или
после прерывания по critical fail.
```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).
---
### `confirm` (хост → таргет)
Ответ оператора на `confirm_request`. Поле `"id"` должно совпадать с `id`
из `confirm_request`.
```json
→ {"type":"confirm","id":"display_red","confirmed":true}
```
Если `"confirmed": false` — таргет записывает `TEST_STATUS_FAIL` для этого шага.
Если ответ пришёл после истечения `timeout_ms` — таргет игнорирует его
(уже перешёл в SKIP).
---
`"id"` должен совпадать с `id` из `confirm_request`. Ответ после `timeout_ms`
игнорируется.
### Ошибки протокола
```json
← {"ok":false,"error":"PARSE_ERR"}
← {"ok":false,"error":"UNKNOWN_CMD"}
← {"ok":false,"error":"UNKNOWN_TEST"}
← {"ok":false,"error":"LINE_TOO_LONG"}
← {"ok":false,"error":"BUSY"}
```
| Код | Причина |
| --------------- | ----------------------------------------------- |
| `PARSE_ERR` | Строка не является валидным JSON-lines запросом |
@ -318,12 +228,8 @@ display (подтвердить цвет), кнопки (нажать кнопк
| `uart_iso` | UART ISO / RS_RX Opto | HIL | ❌ | ✅ | ❌ |
| `opto` | Opto-in EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ |
**Типы тестов:**
- **self** — таргет тестирует периферию самостоятельно, без внешних сигналов.
- **interactive** — требует действия оператора через механизм `confirm_request`.
- **HIL** — требует M5StampPLC для генерации внешних сигналов
(реле, CAN фреймы, UART echo).
**Типы:** **self** — таргет тестирует периферию самостоятельно; **interactive**
требует `confirm_request`; **HIL** — требует M5StampPLC.
---
@ -331,131 +237,96 @@ display (подтвердить цвет), кнопки (нажать кнопк
### uSD
Карта вставляется оператором по запросу. Тест не входит в критический путь.
Pre-confirm обрабатывается `test_runner` до вызова `run()`.
```mermaid
sequenceDiagram
participant H as Хост
participant T as Таргет
```bash
← {"type":"confirm_request","id":"usd","prompt":"Insert microSD card and press OK","timeout_ms":30000}
→ {"type":"confirm","id":"usd","confirmed":true}
← {"type":"test_begin","id":"usd","name":"microSD (SDIO)","critical":false}
← {"type":"progress","test":"usd","step":"card_detect","status":"ok"}
← {"type":"progress","test":"usd","step":"mount","status":"ok"}
← {"type":"progress","test":"usd","step":"write","status":"ok"}
← {"type":"progress","test":"usd","step":"read_compare","status":"ok"}
← {"type":"test_result","id":"usd","status":"pass","ms":741,"detail":""}
T-->>H: {"type":"confirm_request","id":"usd","prompt":"Insert microSD card","timeout_ms":30000}
H->>T: {"type":"confirm","id":"usd","confirmed":true}
T-->>H: {"type":"test_begin","id":"usd","name":"microSD (SDIO)","critical":false}
T-->>H: {"type":"progress","test":"usd","step":"card_detect","status":"ok"}
T-->>H: {"type":"progress","test":"usd","step":"mount","status":"ok"}
T-->>H: {"type":"progress","test":"usd","step":"write","status":"ok"}
T-->>H: {"type":"progress","test":"usd","step":"read_compare","status":"ok"}
T-->>H: {"type":"test_result","id":"usd","status":"pass","ms":741,"detail":""}
```
Если оператор отказался (`"confirmed":false`):
При отказе (`"confirmed":false`) или таймауте:
```bash
```json
← {"type":"test_begin","id":"usd","name":"microSD (SDIO)","critical":false}
← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"operator declined"}
```
Если истёк таймаут (30 с без ответа):
```bash
← {"type":"test_begin","id":"usd","name":"microSD (SDIO)","critical":false}
← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"confirm timeout"}
```
---
### Display (RGB888)
Четыре шага: красный, зелёный, синий, белый. Итог — AND всех подтверждений.
Одновременно верифицируется подсветка (PWM включён).
```mermaid
sequenceDiagram
participant H as Хост
participant T as Таргет
```bash
← {"type":"test_begin","id":"display",...}
← {"type":"confirm_request","id":"display_red","prompt":"Экран залит красным?","timeout_ms":15000}
→ {"type":"confirm","id":"display_red","confirmed":true}
← {"type":"confirm_request","id":"display_green","prompt":"Экран залит зелёным?","timeout_ms":15000}
→ {"type":"confirm","id":"display_green","confirmed":true}
← {"type":"confirm_request","id":"display_blue","prompt":"Экран залит синим?","timeout_ms":15000}
→ {"type":"confirm","id":"display_blue","confirmed":true}
← {"type":"confirm_request","id":"display_white","prompt":"Экран залит белым?","timeout_ms":15000}
→ {"type":"confirm","id":"display_white","confirmed":false}
← {"type":"test_result","id":"display","status":"fail","ms":22103,"detail":"display_white not confirmed"}
T-->>H: {"type":"test_begin","id":"display",...}
T-->>H: {"type":"confirm_request","id":"display_red","prompt":"Экран залит красным?","timeout_ms":15000}
H->>T: {"type":"confirm","id":"display_red","confirmed":true}
T-->>H: {"type":"confirm_request","id":"display_green","prompt":"Экран залит зелёным?","timeout_ms":15000}
H->>T: {"type":"confirm","id":"display_green","confirmed":true}
T-->>H: {"type":"confirm_request","id":"display_blue","prompt":"Экран залит синим?","timeout_ms":15000}
H->>T: {"type":"confirm","id":"display_blue","confirmed":true}
T-->>H: {"type":"confirm_request","id":"display_white","prompt":"Экран залит белым?","timeout_ms":15000}
H->>T: {"type":"confirm","id":"display_white","confirmed":false}
T-->>H: {"type":"test_result","id":"display","status":"fail","ms":22103,"detail":"display_white not confirmed"}
```
### Кнопки
```mermaid
sequenceDiagram
participant H as Хост
participant T as Таргет
T-->>H: {"type":"test_begin","id":"buttons",...}
T-->>H: {"type":"confirm_request","id":"btn1_press","prompt":"Нажмите кнопку Test_But_1","timeout_ms":10000}
Note over T: ждёт bsp_button_get(BTN_TEST_1) == PRESSED<br/>таймаут 10 с
T-->>H: {"type":"confirm_request","id":"btn2_press","prompt":"Нажмите кнопку Test_But_2","timeout_ms":10000}
Note over T: ждёт bsp_button_get(BTN_TEST_2) == PRESSED
T-->>H: {"type":"test_result","id":"buttons","status":"pass","ms":3821,"detail":""}
```
> **Важно:** для теста кнопок таргет **не ждёт** `{"type":"confirm",…}` от
> хоста. Нажатие детектируется прошивкой через `bsp_button`. Хост отображает
> `prompt` и ждёт следующего события от таргета.
---
### Кнопки (Test_But_1 / Test_But_2)
## State machine test_runner
Два шага. Таргет ждёт физического нажатия через `bsp_button`, не через confirm.
`confirm_request` здесь используется как инструкция оператору — ответом является
не JSON, а сам факт нажатия кнопки, который таргет детектирует самостоятельно.
```mermaid
stateDiagram-v2
[*] --> IDLE
```bash
← {"type":"test_begin","id":"buttons",...}
← {"type":"confirm_request","id":"btn1_press","prompt":"Нажмите кнопку Test_But_1","timeout_ms":10000}
[таргет ждёт bsp_button_get(BTN_TEST_1) == PRESSED, таймаут 10 с]
← {"type":"confirm_request","id":"btn2_press","prompt":"Нажмите кнопку Test_But_2","timeout_ms":10000}
[таргет ждёт bsp_button_get(BTN_TEST_2) == PRESSED, таймаут 10 с]
← {"type":"test_result","id":"buttons","status":"pass","ms":3821,"detail":""}
```
IDLE --> PRE_CONFIRM : cmd run / run_all
note right of PRE_CONFIRM : pre_confirm_prompt != NULL?\nprotocol_send_confirm_request()
> **Важно:** для теста кнопок таргет не ждёт `{"type":"confirm",…}` от хоста.
> Хост отображает `prompt` оператору и ждёт следующего события от таргета.
> Нажатие детектируется прошивкой через `bsp_button`, не через CDC.
PRE_CONFIRM --> RUNNING : confirm получен или prompt == NULL
---
RUNNING --> RUNNING : run_all — следующий тест
note right of RUNNING : protocol_send_test_begin()\nmod->init()\nresult = mod->run()\nmod->deinit()\nprotocol_send_test_result()
## Рекомендации для разработчика хостового ПО
### Открытие порта
```bash
1. Найти CDC ACM устройство (VID/PID NXP или зарегистрированный).
2. Открыть порт (любой baud rate — CDC игнорирует его).
3. Ждать строку с "type":"session_start" — таймаут 10 с.
4. Если не получен — переоткрыть порт или перезагрузить таргет.
```
### Чтение событий
```bash
- Читать побайтово или буфером, буферизировать до '\n'.
- Одна строка = одно JSON-сообщение.
- Неизвестное поле "type" — игнорировать (forward-compatibility).
- Парсить минимально: поле "type" определяет дальнейший разбор.
```
### Отправка команд
```bash
- Завершать каждую строку '\n' (не '\r\n').
- Не отправлять следующую команду до получения финального события
предыдущей (test_result или error).
- Исключение: "ping" можно отправлять в любой момент, если таргет не BUSY.
```
### Обработка confirm_request
```bash
1. Получить "confirm_request" → отобразить "prompt" оператору.
2. Дождаться реакции оператора (кнопка в UI, клавиша в TUI).
3. Исключение — тест "buttons": не отправлять confirm, просто ждать
следующего события от таргета.
4. Для всех остальных тестов — отправить:
{"type":"confirm","id":"<тот же id>","confirmed":true/false}
5. Хост может показывать countdown по timeout_ms для UX,
но не обязан — таргет сам завершит по таймауту.
RUNNING --> IDLE : run завершён
RUNNING --> IDLE : run_all завершён\nprotocol_send_summary()
```
---
## Реализация на стороне таргета
### Модули прошивки
```bash
firmware/test/src/
├── main.c — инициализация, главный цикл, вызов cli_process()
├── cli.h / cli.c — IO-слой: буферизация строк, диспатч по "type"
├── protocol.h / .c — сериализация исходящих событий через cli_send()
├── test_module.h — интерфейс тест-модуля (структура test_module_t)
├── test_module.h — интерфейс тест-модуля (struct test_module_t)
├── test_runner.h/.c — реестр тестов, state machine, confirm механизм
└── tests/
├── test_sdram.c
@ -469,64 +340,51 @@ firmware/test/src/
└── test_opto.c
```
### State machine test_runner
```
┌─────────────────────────────────────────┐
│ IDLE │◄──────────────────┐
│ Ждём команду от хоста │ │
└───────────────┬─────────────────────────┘ │
│ cmd: run / run_all │
▼ │
┌─────────────────────────────────────────┐ │
│ PRE_CONFIRM │ │
│ pre_confirm_prompt != NULL? │ │
│ → protocol_send_confirm_request() │ │
└───────────────┬─────────────────────────┘ │
│ confirm получен / NULL │
▼ │
┌─────────────────────────────────────────┐ │
│ RUNNING │ │
│ protocol_send_test_begin() │ │
│ mod->init() если задан │ │
│ result = mod->run() │ │
│ mod->deinit() если задан │ │
│ protocol_send_test_result() │ │
└───────────────┬─────────────────────────┘ │
│ │
├── run_all: следующий тест ──────────────────┤
│ │
└── run_all завершён: protocol_send_summary() ┘
run: сразу → IDLE
```
### Добавление нового теста
1. Создать `firmware/test/src/tests/test_foo.c` с реализацией `test_result_t test_foo_run(void)`.
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,
};
```
3. Добавить `&k_test_foo` в реестр `test_runner.c` — одна строка.
```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,
};
```
3. Добавить `&k_test_foo` в реестр `test_runner.c`.
4. Добавить `tests/test_foo.c` в `CMakeLists.txt` таргета.
---
## Рекомендации для разработчика хостового ПО
**Открытие порта:** найти CDC ACM устройство → открыть порт → ждать
`"type":"session_start"` (таймаут 10 с) → при отсутствии переоткрыть порт.
**Чтение событий:** буферизировать до `'\n'`; одна строка = одно сообщение;
неизвестный `"type"` — игнорировать (forward-compatibility).
**Отправка команд:** завершать каждую строку `'\n'` (не `'\r\n'`); не
отправлять следующую команду до `test_result` или `error` от предыдущей.
Исключение: `"ping"` можно отправлять в любой момент, если таргет не BUSY.
**Обработка `confirm_request`:** отобразить `prompt` → ждать реакции
оператора → отправить `{"type":"confirm","id":"<тот же id>","confirmed":true/false}`.
Исключение — тест `buttons`: не отправлять `confirm`, просто ждать следующего
события от таргета.
---
## Версионирование протокола
Поле `"fw"` в `session_start` — версия прошивки, а не версия протокола.
При несовместимых изменениях протокола (новое обязательное поле, изменение
семантики существующего) — bumping `FIRMWARE_TEST_VERSION` в `protocol.h`
с соответствующим обновлением этого документа.
Хост должен проверять `"fw"` и предупреждать оператора при несовпадении
ожидаемой версии.
Поле `"fw"` в `session_start` — версия прошивки. При несовместимых изменениях
протокола — bump `FIRMWARE_TEST_VERSION` в `protocol.h` с обновлением этого
документа. Хост должен проверять `"fw"` и предупреждать оператора при
несовпадении ожидаемой версии.

View file

@ -2,30 +2,31 @@
## Обзор стека
```bash
devcontainer хост
───────────────────────────────── ────────────────────────────────────
tests/target/<name>/ tools/hil/
main.c ← C-прошивка с CLI test_<name>.py ← pytest-тесты
CMakeLists.txt conftest.py ← фикстуры (общие)
m5/agent.py ← агент M5 (если нужен)
CMakePresets.json
target-debug-build just/host.just
└── targets: [test_<name>] hil-run, hil-<name>
```mermaid
flowchart LR
subgraph DC["Devcontainer"]
C["tests/target/&lt;name&gt;/\nmain.c — C-прошивка с CLI\nCMakeLists.txt"]
CP["CMakePresets.json\ntarget-debug-build"]
JB["just/build.just\nbuild-hil"]
C --> CP --> JB
end
just/build.just
build-hil
subgraph Host["Хост"]
PY["tools/hil/\ntest_&lt;name&gt;.py — pytest\nconftest.py — фикстуры"]
JH["just/host.just\nhil-run, hil-&lt;name&gt;"]
PY --> JH
end
```
Три типа тестов:
| Тип | Использует M5 | Запуск | Когда применять |
| ----------------- | ------------- | --------------------- | ----------------------------------------------------- |
| **Базовый** | Нет | `hil-run` | Тестирование UART CLI, алгоритмов, таймингов |
| **С M5** | Да | `hil-run` | Тестирование GPIO, оптовходов, реле, питания |
| **Базовый** | Нет | `hil-run` | UART CLI, алгоритмы, тайминги |
| **С M5** | Да | `hil-run` | GPIO, оптовходы, реле, питание |
| **Интерактивный** | Нет / Да | `hil-run-interactive` | Периферия требует действий оператора: кнопки, дисплей |
Интерактивные тесты помечаются `@pytest.mark.interactive` и **никогда не входят в `hil-run`** — они требуют живого оператора и не пригодны для CI.
Интерактивные тесты помечаются `@pytest.mark.interactive` и **не входят в `hil-run`**.
---
@ -42,7 +43,7 @@ just/build.just
#define CLI_BAUD_RATE 115200U
#define CLI_LINE_MAX 128U
#define CLI_RX_TIMEOUT 100U /* мс — увеличить если нужен частый process() */
#define CLI_RX_TIMEOUT 100U
static size_t cli_read_line(uint8_t *p_buf, size_t max_len)
{
@ -77,7 +78,6 @@ int main(void)
bsp_uart_host_init(CLI_BAUD_RATE);
bsp_led_on(LED_HEARTBEAT);
/* Шлём READY пока хост не открыл порт */
while (bsp_uart_host_rx_available() == 0U) {
bsp_uart_host_write_str("READY\r\n");
bsp_delay(200U);
@ -85,7 +85,6 @@ int main(void)
static uint8_t s_line_buf[CLI_LINE_MAX];
for (;;) {
/* Если тест использует прерывания/process() — вызывать здесь */
size_t len = cli_read_line(s_line_buf, sizeof(s_line_buf));
if (len > 0U) cli_process_line((const char *)s_line_buf);
}
@ -132,7 +131,7 @@ add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
```cmake
add_subdirectory(host_uart)
add_subdirectory(hil_opto)
add_subdirectory(<name>) # ← добавить строку
add_subdirectory(<name>) # ← добавить
```
---
@ -153,13 +152,13 @@ add_subdirectory(<name>) # ← добавить строку
---
## Шаг 4 — Сборка
## Шаг 4 — Собрать
```bash
# В devcontainer:
just build::build-hil
# Проверить что новый таргет собрался:
# Проверить:
ls build/target-debug/tests/target/<name>/test_<name>.elf
```
@ -167,16 +166,10 @@ ls build/target-debug/tests/target/<name>/test_<name>.elf
## Шаг 5 — `conftest.py`: добавить фикстуры
Открыть `tools/hil/conftest.py` и добавить:
1. Фикстуру загрузки `loaded_<n>` в конец раздела загрузок.
2. Одну строку в `_UART_FIXTURE_MAP` — фабрика `_make_uart_fixture` автоматически
создаст фикстуру `uart_<n>` через контекстный менеджер `_uart_context`.
### Базовый тест (без M5)
```python
# 1. Фикстура загрузки — добавить в раздел loaded_*
# 1. Фикстура загрузки
@pytest.fixture(scope="module")
def loaded_<n>(request: pytest.FixtureRequest) -> None:
_load_elf(
@ -184,22 +177,19 @@ def loaded_<n>(request: pytest.FixtureRequest) -> None:
Path(cfg.BUILD_DIR) / "tests/target/<n>/test_<n>.elf",
)
# 2. UART-фикстура — добавить одну строку в словарь
# 2. UART-фикстура — одна строка в словарь
_UART_FIXTURE_MAP = {
...
"uart_<n>": "loaded_<n>", # ← добавить
"uart_<n>": "loaded_<n>",
}
```
### Тест с M5 (GPIO, реле, питание)
### Тест с M5
```python
# 1. Фикстура загрузки — зависимость от m5 гарантирует питание
# 1. Зависимость от m5 гарантирует что питание включено до загрузки ELF
@pytest.fixture(scope="module")
def loaded_<n>(request: pytest.FixtureRequest, m5: M5Agent) -> None:
"""
Зависит от m5 — питание таргета уже включено к моменту загрузки ELF.
"""
_load_elf(
request,
Path(cfg.BUILD_DIR) / "tests/target/<n>/test_<n>.elf",
@ -208,23 +198,18 @@ def loaded_<n>(request: pytest.FixtureRequest, m5: M5Agent) -> None:
# 2. UART-фикстура — та же одна строка
_UART_FIXTURE_MAP = {
...
"uart_<n>": "loaded_<n>", # ← добавить
"uart_<n>": "loaded_<n>",
}
```
**Правило:** если тест управляет железом через M5 — `loaded_<n>` должен явно
зависеть от `m5`. Это гарантирует что питание включено до того как pyOCD
попытается подключиться к MCU.
> Ручное написание `uart_<n>` фикстур больше не требуется — фабрика
> `_make_uart_fixture` создаёт фикстуру с `_uart_context` (контекстный менеджер,
> гарантирует `ser.close()` при любом исходе).
зависеть от `m5`, иначе pyOCD попытается подключиться до включения питания.
---
## Шаг 6 — `tools/hil/test_<name>.py`
### Базовый тест (без M5)
### Базовый тест
```python
"""test_<name>.py — HIL тест <что тестируем>."""
@ -239,23 +224,21 @@ class Test<Name>:
self.ser = uart_<name>
def test_ping(self):
"""Базовая проверка канала."""
assert uart_cmd(self.ser, "PING") == "PONG"
def test_something(self):
resp = uart_cmd(self.ser, "MY_CMD")
assert resp == "EXPECTED"
assert uart_cmd(self.ser, "MY_CMD") == "EXPECTED"
```
### Тест с M5
```python
"""test_<name>.py — HIL тест <что тестируем> через M5StampPLC."""
"""test_<name>.py — HIL тест через M5StampPLC."""
import time
import pytest
from conftest import uart_cmd
SETTLE_S = 0.15 # ждать после переключения реле
SETTLE_S = 0.15
class Test<Name>:
@ -264,24 +247,19 @@ class Test<Name>:
def _setup(self, uart_<name>, m5):
self.ser = uart_<name>
self.m5 = m5
self.m5.opto_all_off() # или другой сброс состояния стенда
self.m5.opto_all_off()
time.sleep(SETTLE_S)
def test_ping(self):
assert uart_cmd(self.ser, "PING") == "PONG"
def test_m5_ping(self):
self.m5.ping()
def test_something_with_relay(self):
self.m5.opto_set(1, True)
time.sleep(SETTLE_S)
assert uart_cmd(self.ser, "READ_INPUT") == "ACTIVE"
```
**Важно про таймауты:** после переключения реле нужно ждать:
реле (~10 мс) + оптопара (~0.1 мс) + дебаунс прошивки + один цикл `process()`.
Используй активное ожидание вместо фиксированного `sleep` там где важна скорость:
Для критичных к скорости тестов — активное ожидание вместо фиксированного `sleep`:
```python
def wait_until(ser, cmd, expected, timeout_s=1.0):
@ -293,22 +271,20 @@ def wait_until(ser, cmd, expected, timeout_s=1.0):
raise TimeoutError(f"Ожидали {expected!r} от '{cmd}'")
```
### Интерактивный тест (оператор нажимает кнопки / смотрит на дисплей)
Добавить маркер на класс. Для ввода использовать `/dev/tty` напрямую — `input()` не работает под захватом pytest даже с `-s`:
### Интерактивный тест
```python
"""test_<name>.py — интерактивный HIL-тест <что тестируем>."""
"""test_<name>.py — интерактивный HIL-тест."""
import time
import pytest
from conftest import uart_cmd
SETTLE_S = 0.10 # ждать после действия оператора (debounce и т.п.)
SETTLE_S = 0.10
def _operator_prompt(msg: str) -> None:
"""Вывести подсказку и дождаться Enter от оператора.
Читает /dev/tty напрямую — работает независимо от захвата pytest."""
"""Вывести подсказку и ждать Enter. Читает /dev/tty напрямую — работает
независимо от захвата pytest."""
with open("/dev/tty", "w") as tty_out:
tty_out.write(f"\n >>> {msg}\n Нажмите Enter когда готово...\n")
tty_out.flush()
@ -332,14 +308,7 @@ class Test<Name>:
assert uart_cmd(self.ser, "MY_CMD") == "EXPECTED"
```
**Запуск интерактивных тестов:**
```bash
just host::hil-run-interactive # все интерактивные
just host::hil-button # конкретный интерактивный
```
**Правило:** интерактивные тесты **не добавлять** в `hil-run` — они входят только в `hil-run-interactive`.
Запуск: `just host::hil-run-interactive` или `just host::hil-<name>` (с флагом `-s`).
---
@ -365,86 +334,50 @@ hil-<n>:
---
## Полный цикл
```bash
# 1. devcontainer — собрать прошивку
just build::build-hil
# 2. хост — убедиться что стенд готов (если тест использует M5)
just host::m5-deploy # если менялся agent.py
just host::m5-scan # убедиться что M5 видна
# 3. хост — запустить только новый тест
just host::hil-<name>
# 4. хост — загрузить ELF вручную без тестов (для отладки)
uv run --directory tools/hil python load_and_run.py \
build/target-debug/tests/target/<name>/test_<name>.elf
# 5. хост — запустить один тест
uv run --directory tools/hil pytest test_<name>.py::Test<Name>::test_ping -v
```
---
## Как работают фикстуры
### Цепочка зависимостей
```mermaid
flowchart TB
TF["test_foo()"]
SU["_setup\n(function scope, autouse)"]
UN["uart_&lt;n&gt;\n(module scope)"]
LN["loaded_&lt;n&gt;\n(module scope)"]
M5["m5\n(module scope, если нужен)"]
```bash
test_foo()
└── _setup (function scope, autouse)
├── uart_<n> (module scope) ← открыт один раз на весь файл
│ └── loaded_<n> ← ELF загружен один раз
│ └── m5 ← (если нужен) питание включено
└── m5 (module scope) ← (если нужен напрямую в тесте)
TF --> SU
SU --> UN --> LN
LN --> M5
```
`scope=module` — фикстура создаётся один раз на весь тест-файл, уничтожается
после последнего теста. ELF грузится один раз, порт открывается один раз.
`scope=module` — фикстура создаётся один раз на весь тест-файл. ELF грузится
один раз, порт открывается один раз.
### Порядок при запуске нескольких файлов
```bash
pytest 01_test_uart.py test_<name>.py
01_test_uart.py test_<name>.py
───────────────────── ─────────────────────
loaded_host_uart m5 ← создаётся
uart ← создаётся loaded_<name>
test_ping uart_<name> ← создаётся
test_echo test_ping
uart.close() test_something
uart_<name>.close()
m5 teardown → power(False)
```
Каждый файл — своя загрузка ELF, свой UART-сеанс. MCU перезагружается между файлами.
**Порядок при нескольких файлах:** каждый файл — своя загрузка ELF, свой
UART-сеанс. MCU перезагружается между файлами.
---
## Чеклист
### Автоматический тест (базовый или с M5)
### Автоматический тест
```bash
[ ] tests/target/<n>/main.c — C-прошивка с CLI + READY-паттерн
[ ] tests/target/<n>/CMakeLists.txt — сборка с bsp_boot_ram
[ ] tests/target/<n>/main.c — C-прошивка с CLI + READY-паттерн
[ ] tests/target/<n>/CMakeLists.txt — сборка с bsp_boot_ram
[ ] tests/target/CMakeLists.txt — add_subdirectory(<n>)
[ ] CMakePresets.json — добавить test_<n> в targets
[ ] tools/hil/conftest.py — loaded_<n> + строка в _UART_FIXTURE_MAP
[ ] tools/hil/test_<n>.py — pytest-тесты
[ ] tools/hil/test_<n>.py — pytest-тесты
[ ] just/host.just — рецепт hil-<n> (опционально)
[ ] just build::build-hil — зелёная сборка
[ ] just host::hil-<n> — зелёный прогон
[ ] just host::hil-<n> — зелёный прогон
```
### Интерактивный тест (дополнительно к базовому чеклисту)
### Дополнительно для интерактивного теста
```bash
[ ] @pytest.mark.interactive — пометить класс в test_<n>.py
[ ] just/host.just — рецепт hil-<n> с флагом -s
[ ] just host::hil-run-interactive — зелёный прогон
[ ] убедиться что just host::hil-run — NOT в выборке (маркер исключает)
[ ] убедиться что just host::hil-run — НЕ включает этот тест
```

View file

@ -1,376 +1,232 @@
# Добавление нового host-теста
# Отладка прошивок через SWD + GDB
## Обзор стека
## Обзор архитектуры
Отладка построена на пробросе GDB-сервера с хоста в devcontainer по TCP. Это
позволяет держать весь инструментарий сборки и языковой сервер внутри контейнера,
не проводя USB-пробник внутрь Docker.
```mermaid
flowchart LR
subgraph Host["Хост (macOS / Linux)"]
DS["just host::debug-server\npyocd gdbserver :3333"]
ML["MCU-Link (CMSIS-DAP)"]
DS --> ML
end
subgraph DC["Devcontainer"]
CD["cortex-debug\n(VSCode F5)"]
GDB["arm-none-eabi-gdb\nсимволы из .elf"]
CD --> GDB
end
Board["MIMXRT1052\nFlash / SDRAM\nSEGGER RTT буфер"]
GDB -->|"TCP host.docker.internal:3333"| DS
ML -->|"SWD"| Board
```
**Ключевой принцип:** `pyocd gdbserver` слушает на `0.0.0.0:3333`. Из контейнера
GDB подключается через `host.docker.internal:3333` — специальный DNS-алиас Docker,
резолвится в IP хост-машины.
---
## Компоненты
### На хосте
| Компонент | Роль | Источник |
| --------------------------------- | ------------------------------- | -------------------------- |
| `pyocd` | GDB-сервер + flash-программатор | `tools/hil/uv.lock` |
| `MCU-Link` | CMSIS-DAP v2 пробник | USB к плате |
| `just host::debug-server` | Запуск GDB-сервера | `just/host.just` |
| `just host::flash-swd-*` | Прошивка через SWD | `just/host.just` |
| `tools/host/flash_swd.py` | Сборка FCB+HAB образа и запись | `tools/host/` |
| `tools/host/dcd/w25q128_fdcb.bin` | FCB для W25Q128 (Quad SPI) | NXP SecureProvisioningTool |
### В devcontainer
| Компонент | Роль |
| -------------------------------------- | ---------------------------------------------- |
| `arm-none-eabi-gdb` | GDB клиент, подключается к серверу на хосте |
| `cortex-debug` (VSCode extension) | UI для GDB: брейкпоинты, стек, регистры |
| `.vscode/launch.json` | Конфигурации запуска отладки |
| `.vscode/tasks.json` | `preLaunchTask` — пересборка ELF перед стартом |
| `build/Debug/*.elf` | Символы для GDB (DWARF debug info) |
| `bsp/generated/startup/MIMXRT1052.xml` | SVD — описание регистров периферии |
### Конфигурация
Параметры отладки задаются в `.env`:
```bash
devcontainer
─────────────────────────────────────────────────────────────────
tests/host/<n>/test_<n>.c ← тест (Unity + опционально fff)
tests/host/CMakeLists.txt ← регистрация через add_host_test()
tests/host/mocks/ ← stub-хедеры NXP SDK (если нужны)
CMakePresets.json just/build.just
host-debug-build test-host
└── targets: [test_<n>] cmake --build + ctest
GDB_PORT=3333
PYOCD_TARGET=mimxrt1050_quadspi
PYOCD_FREQUENCY=4000000
FCB_PATH=tools/host/dcd/w25q128_fdcb.bin
```
---
## Шаг 0 — Определить категорию теста
## Поддерживаемые прошивки
Перед написанием кода определи к какой категории относится модуль:
| Конфигурация 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 |
| Категория | Описание | Инструментарий |
| --------- | -------------------------------------------------------------- | ------------------------- |
| **A** | Нет вызовов NXP SDK: алгоритмы, парсеры, FSM, структуры данных | Unity |
| **B** | BSP-модуль вызывает `fsl_*.h`, USB-стек и т.д. | Unity + fff + stub-хедеры |
**Признак категории A:** в `.c` файле модуля нет ни одного `#include "fsl_*.h"`.
**Признак категории B:** есть хотя бы один такой include.
Все три — XIP-прошивки, исполняются из QuadSPI NOR Flash (`0x60000000`).
---
## Шаг 1 — Создать тестовый файл
## Режимы запуска отладки
### Категория A — платформонезависимый модуль
```c
/* tests/host/<n>/test_<n>.c */
#include "unity.h"
#include "<n>.h" /* тестируемый модуль */
void setUp(void) { /* сброс состояния перед каждым тестом */ }
void tearDown(void) { /* очистка после каждого теста */ }
void test_something(void)
{
/* Arrange */
int input = 42;
/* Act */
int result = module_process(input);
/* Assert */
TEST_ASSERT_EQUAL(expected, result);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_something);
return UNITY_END();
}
```
### Категория B — BSP-модуль с fff-фейками
Порядок `#include` принципиален — нарушение порядка вызовет ошибки компиляции:
```c
/* tests/host/<n>/test_<n>.c */
#include "unity.h"
#include "fff.h"
DEFINE_FFF_GLOBALS; /* 1. ровно один раз на весь .c файл */
#include "fsl_<driver>.h" /* 2. stub-хедер с типами (из mocks/) */
/* 3. объявить фейки для всех SDK-функций, которые вызывает тестируемый модуль */
FAKE_VOID_FUNC(SDK_Function_A, ArgType1, ArgType2);
FAKE_VALUE_FUNC(status_t, SDK_Function_B, ArgType1);
#include "bsp/<module>.h" /* 4. тестируемый модуль — всегда последним */
void setUp(void)
{
RESET_FAKE(SDK_Function_A);
RESET_FAKE(SDK_Function_B);
FFF_RESET_HISTORY();
/* при необходимости задать дефолтные return_val */
}
void tearDown(void) { }
void test_something(void)
{
/* Arrange: настроить поведение фейков */
SDK_Function_B_fake.return_val = kStatus_Success;
/* Act */
bsp_status_t status = bsp_module_do_something();
/* Assert: проверить результат и вызовы */
TEST_ASSERT_EQUAL(BSP_OK, status);
TEST_ASSERT_EQUAL(1, SDK_Function_A_fake.call_count);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_something);
return UNITY_END();
}
```
---
## Шаг 2 — Stub-хедеры (только категория B)
Если тестируемый модуль использует NXP SDK хедеры которых ещё нет в `tests/host/mocks/` — нужно создать stub.
### Что такое stub-хедер и зачем он нужен
NXP SDK хедеры (`fsl_gpio.h` и др.) тянут платформенные регистровые определения для Cortex-M7 — они не компилируются на хосте. Stub-хедер в `tests/host/mocks/` содержит только минимально необходимые типы и сигнатуры функций. CMake подключает `mocks/` **до** SDK, поэтому компилятор находит stub раньше оригинала.
### Шаблон stub-хедера
Добавляй в stub только то, что реально используется в тестируемом `.c` файле:
```c
/* tests/host/mocks/fsl_<driver>.h */
#pragma once
#include <stdint.h>
/* Минимально необходимые типы */
typedef struct { uint32_t reserved[64]; } DRIVER_Type;
typedef enum {
kStatus_Success = 0,
kStatus_Fail = 1,
} status_t;
/* Сигнатуры функций — реализации предоставляет fff */
void SDK_Function_A(DRIVER_Type *base, uint32_t arg);
status_t SDK_Function_B(DRIVER_Type *base, const uint8_t *data, size_t len);
```
### Уже существующие stubs в `tests/host/mocks/`
| Файл | Что заменяет | Используется в |
| ------------ | ----------------------------- | -------------- |
| `fsl_gpio.h` | GPIO драйвер | `test_bsp_led` |
| `pin_mux.h` | Макросы пинов из `generated/` | `test_bsp_led` |
| `board.h` | `board_hw_init()` | `test_bsp_led` |
Если нужный stub уже есть — ничего создавать не нужно, просто укажи `mocks/` в `MOCKS` аргументе `add_host_test()`.
---
## Шаг 3 — Зарегистрировать тест в `tests/host/CMakeLists.txt`
Используй функцию `add_host_test()`. Она создаёт исполняемый файл и регистрирует его в CTest:
```cmake
# Категория A — без mocks
add_host_test(
NAME test_<n>
SOURCES <n>/test_<n>.c
${PROJECT_SOURCE_DIR}/<path_to_module>/<module>.c
INCLUDES
${PROJECT_SOURCE_DIR}/<path_to_module>/include
)
# Категория B — с mocks
add_host_test(
NAME test_<n>
SOURCES <n>/test_<n>.c
${PROJECT_SOURCE_DIR}/bsp/<module>/src/<module>.c
INCLUDES
${PROJECT_SOURCE_DIR}/bsp/<module>/include
MOCKS
${BSP_MOCKS_DIR} # = tests/host/mocks/
)
```
### Аргументы `add_host_test()`
| Аргумент | Обязателен | Описание |
| ---------- | ---------- | --------------------------------------------------------------- |
| `NAME` | ✓ | Имя исполняемого файла и теста в CTest |
| `SOURCES` | ✓ | Тестовый `.c` + исходники тестируемых модулей |
| `INCLUDES` | — | Дополнительные include-пути (для `#include "bsp/led.h"` и т.д.) |
| `MOCKS` | — | Директории со stub-хедерами (подключаются с высшим приоритетом) |
`lib_external` (Unity + fff) подключается автоматически — добавлять не нужно.
---
## Шаг 4 — Добавить таргет в `CMakePresets.json`
```json
{
"name": "host-debug-build",
"configurePreset": "host-debug",
"targets": [
"test_bsp_led",
"test_ring_buffer",
"test_timeout_pattern",
"uart_host_mock_example",
"test_<n>"
]
}
```
То же самое для `host-release-build` если нужен Release-прогон.
---
## Шаг 5 — Запустить
### Режим А — прошивка уже в Flash
```bash
# Сборка + все тесты одной командой (devcontainer)
just build::test-host
# 1. Хост — запустить GDB-сервер (оставить в отдельном терминале)
just host::debug-server
# Только новый тест
ctest --preset host-debug-test -R test_<n> -V
# Напрямую — виден полный вывод Unity без CTest-обёртки
./build/host-debug/tests/host/test_<n>
# 2. DevContainer — VSCode
# Run & Debug (Ctrl+Shift+D) → выбрать конфигурацию → F5
```
---
GDB сбрасывает MCU, загружает символы из ELF и останавливается на входе
в `main`. Flash не перезаписывается.
## Справочник: Unity assertions
```c
/* Целые числа */
TEST_ASSERT_EQUAL(expected, actual)
TEST_ASSERT_EQUAL_INT8 / INT16 / INT32 / UINT8 / UINT32(expected, actual)
TEST_ASSERT_NOT_EQUAL(expected, actual)
TEST_ASSERT_INT_WITHIN(delta, expected, actual)
/* Булевые / указатели */
TEST_ASSERT_TRUE(condition)
TEST_ASSERT_FALSE(condition)
TEST_ASSERT_NULL(pointer)
TEST_ASSERT_NOT_NULL(pointer)
/* Строки / память */
TEST_ASSERT_EQUAL_STRING(expected, actual)
TEST_ASSERT_EQUAL_MEMORY(expected, actual, len)
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, actual, len)
/* Явный провал / пропуск */
TEST_FAIL_MESSAGE("причина")
TEST_IGNORE_MESSAGE("в процессе")
```
---
## Справочник: fff-фейки
### Объявление
```c
FAKE_VOID_FUNC(func, ArgType1, ArgType2); /* void-функция */
FAKE_VALUE_FUNC(RetType, func, ArgType1, ArgType2);/* с возвращаемым значением */
FAKE_VOID_FUNC_VARARG(func, const char *, ...); /* variadic */
```
### Управление поведением
```c
/* Фиксированное возвращаемое значение */
func_fake.return_val = kStatus_Fail;
/* Последовательность значений */
status_t seq[] = {kStatus_Success, kStatus_Success, kStatus_Fail};
SET_RETURN_SEQ(func, seq, 3);
/* Кастомная реализация — высший приоритет, перекрывает return_val */
func_fake.custom_fake = my_impl;
```
### Проверка вызовов
```c
TEST_ASSERT_EQUAL(2, func_fake.call_count); /* сколько раз вызвана */
TEST_ASSERT_EQUAL(expected, func_fake.arg0_val); /* аргумент последнего вызова */
TEST_ASSERT_EQUAL_PTR(func, fff.call_history[0]); /* порядок вызовов */
TEST_ASSERT_EQUAL(0, func_fake.call_count); /* не была вызвана */
```
### Сброс в setUp
```c
void setUp(void)
{
RESET_FAKE(func_a); /* сбрасывает счётчик, историю, return_val, custom_fake */
RESET_FAKE(func_b);
FFF_RESET_HISTORY(); /* сбрасывает глобальную историю порядка вызовов */
}
```
---
## Ловушки
**Dangling pointer из `arg_history[]`.**
`arg_history[]` хранит указатели, не копии. Если функция получала указатель на стековую переменную — после возврата это UB. Использовать `custom_fake` с копированием по значению:
```c
static gpio_pin_config_t s_captured;
static void capture(GPIO_Type *base, uint32_t pin, const gpio_pin_config_t *cfg)
{
s_captured = *cfg; /* копия по значению пока стек ещё жив */
}
void setUp(void) {
RESET_FAKE(GPIO_PinInit);
GPIO_PinInit_fake.custom_fake = capture;
bsp_led_init();
}
void test_init_output(void) {
TEST_ASSERT_EQUAL(kGPIO_DigitalOutput, s_captured.direction);
}
```
**`static` функции не мокаются.**
FFF не видит `static` функции снаружи translation unit. Решение — compile-time seam:
```c
#ifdef UNIT_TEST
void internal_fn(void); /* тест подставит свою реализацию */
#else
static void internal_fn(void) { ... }
#endif
```
**Отсутствие стандартных хедеров в BSP.**
BSP-модули должны явно включать `<stdint.h>`, `<stdbool.h>`, `<stddef.h>` — не полагаться на транзитивное подтягивание через NXP SDK. На хосте этот транзит отсутствует, компиляция упадёт с `undeclared identifier 'size_t'`.
---
## Полный цикл
### Режим Б — прошить через SWD, затем отладить
```bash
# 1. Создать тестовый файл
tests/host/<n>/test_<n>.c
# 1. DevContainer
just build::hab-firmware-test-debug
# 2. Создать stub-хедер (если категория B и stub не существует)
tests/host/mocks/fsl_<driver>.h
# 2. Хост
just host::flash-swd-test-debug
# 3. Добавить вызов add_host_test() в
tests/host/CMakeLists.txt
# 3. ⚡ Power cycle платы (обязательно)
# 4. Добавить "test_<n>" в targets в
CMakePresets.json ← host-debug-build и host-release-build
# 4. Хост
just host::debug-server
# 5. Запустить в devcontainer
just build::test-host
# 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
[ ] Определена категория (A или B)
[ ] tests/host/<n>/test_<n>.c — тест с main(), setUp(), tearDown()
[ ] tests/host/mocks/fsl_<driver>.h — stub (только категория B, если нет)
[ ] tests/host/CMakeLists.txt — add_host_test(NAME test_<n> ...)
[ ] CMakePresets.json — добавить test_<n> в host-debug-build
[ ] just build::test-host — зелёный прогон
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 "SEGGER_RTT.h"
SEGGER_RTT_printf(0, "value = %d\n", value);
```
---
## 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
# 1. Убедиться что в .devcontainer/devcontainer.json есть (для Linux):
# "runArgs": ["--add-host=host.docker.internal:host-gateway"]
# 2. Залить прошивку
just host::flash-test-debug
# 3. Хост — запустить GDB-сервер
just host::debug-server
# 4. DevContainer — VSCode
# Ctrl+Shift+D → 🐛 Debug: firmware_test → F5
```
---
## Дерево файлов отладки
```bash
.
├── .env # GDB_PORT, PYOCD_TARGET, PYOCD_FREQUENCY, FCB_PATH
├── .vscode/
│ ├── launch.json # cortex-debug конфигурации (3 проекта)
│ └── tasks.json # preLaunchTask: build:*-debug
├── bsp/generated/startup/
│ └── MIMXRT1052.xml # SVD — регистры периферии
├── just/
│ └── host.just # debug-server, flash-swd-*
└── tools/
├── hil/ # uv-проект с pyocd
└── host/
├── flash_swd.py # FCB + HAB → Flash через pyOCD
└── dcd/
└── w25q128_fdcb.bin # FCB для W25Q128 Quad SPI
```

View file

@ -15,6 +15,7 @@ add_executable(
src/tests/test_qspi.c
src/tests/test_usd.c
src/tests/test_display.c
src/tests/test_buttons.c
${BSP_GENERATED}/clock_config.c
${BSP_STARTUP_FILE}
${BSP_SYSCALLS_FILE})
@ -38,6 +39,7 @@ target_link_libraries(
${TARGET_NAME}
PRIVATE bsp_board
bsp_led
bsp_button
bsp_display
bsp_tick
bsp_boot_xip

View file

@ -1,6 +1,6 @@
# firmware_test — Plan of Development
# firmware_test — План разработки
> Версия: 0.4 | Обновлён после завершения Этапов 13 (bsp_sdram + bsp_qspi_flash).
> Версия: 0.5 | Обновлён после завершения Этапа 5 (display + buttons).
---
@ -18,23 +18,22 @@
## Текущий статус
| Компонент | Статус | Примечание |
| -------------------------- | ------ | -------------------------------------- |
| `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` | ✅ | W25Q64/128/256/512, ITCM, IRQ lock |
| `test_qspi` | ✅ | JEDEC + erase + rw + addr range |
| `bsp_usd` | ✅ | bsp_sd + FatFS (firmware_test_fatfs) |
| `test_usd` | ✅ | pre_confirm + 4 шага + progress events |
| Display test | ⬜ | Этап 5 |
| Button test | ⬜ | Этап 5 |
| CAN test | ⬜ | Этап 6 (bsp_can ✅) |
| UART TTL test | ⬜ | Этап 6 (bsp_uart_host ✅) |
| UART ISO test | ⬜ | Этап 6 |
| Opto test | ⬜ | Этап 6 (bsp_opto ✅) |
| Provisioning | ⬜ | Этап 7 |
| Компонент | Статус | Примечание |
| ------------------------------ | ------ | -------------------------------------------- |
| `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` | ✅ | W25Q64/128/256/512, ITCM, IRQ lock |
| `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 |
| CAN test | ⬜ | Этап 6 (bsp_can ✅) |
| UART TTL test | ⬜ | Этап 6 (bsp_uart_host ✅) |
| UART ISO test | ⬜ | Этап 6 |
| Opto test | ⬜ | Этап 6 (bsp_opto ✅) |
| Provisioning | ⬜ | Этап 7 |
---
@ -52,10 +51,13 @@
- **Порядок init в main.c:** `bsp_qspi_init()` до `bsp_tick_init()`.
- **IR и RTC:** не реализуются.
- **Производственный runner:** Вариант D — отдельный `tools/production/` без pytest.
- **bsp_button_init():** вызывается в `init()` тест-модуля, не в `main.c`.
- **Тест дисплея:** 4 цвета + 2 ротации (TFT ≠ TFT4). Таймаут confirm 15 с → FAIL.
- **Тест кнопок:** физическая детекция через `bsp_button`. Хост не отправляет JSON confirm. Таймаут 10 с → SKIP.
---
## Этап 4 — `bsp_usd` + `test_usd`
## Этап 4 — bsp_sd + test_usd ✅
### Аппаратный контекст
@ -64,66 +66,9 @@
| Интерфейс | USDHC (SDIO) |
| Карта | microSD, вставляется оператором перед тестом |
| Файловая система | FatFS (SDK middleware) |
| Детект карты | GPIO (CD pin) или опрос через USDHC status |
| Детект карты | `USDHC_GetPresentStatusFlags` |
### BSP API (предварительно)
```c
/* bsp/usd/include/bsp/usd.h */
typedef enum {
BSP_USD_OK = 0,
BSP_USD_ERR_NO_CARD, /* карта не вставлена */
BSP_USD_ERR_INIT, /* USDHC или FatFS init failed */
BSP_USD_ERR_MOUNT, /* f_mount() failed */
BSP_USD_ERR_RW, /* read/write/compare failed */
} bsp_usd_status_t;
bsp_usd_status_t bsp_usd_init(void);
bsp_usd_status_t bsp_usd_is_card_present(void);
bsp_usd_status_t bsp_usd_test_rw(void); /* write + read + compare тестового файла */
void bsp_usd_deinit(void);
```
### test_usd — шаги
| Шаг | Действие | Время |
| ----------------- | ------------------------------ | -------- |
| 1: Card detect | `bsp_usd_is_card_present()` | < 1 мс |
| 2: Mount | `f_mount()` — FAT/exFAT | < 200 мс |
| 3: Write | Записать 4 KB тестовый файл | < 500 мс |
| 4: Read + Compare | Прочитать и сравнить побайтово | < 200 мс |
| 5: Unmount | `f_unmount()` | < 50 мс |
### Интерактивность
Тест помечен `requires_hil = false`, `pre_confirm_prompt = "Вставьте microSD и нажмите OK"`.
test_runner ждёт `{"type":"confirm","id":"usd_insert","confirmed":true}` до вызова `run()`.
Отказ или таймаут 30 с`TEST_STATUS_SKIP`.
### Файлы
```
bsp/usd/
├── CMakeLists.txt
├── README.md
├── include/bsp/usd.h
└── src/usd.c
firmware/test/src/tests/test_usd.c
```
### CMake
```cmake
# bsp/usd/CMakeLists.txt
target_link_libraries(bsp_usd
PUBLIC bsp_status
PRIVATE bsp_board sdk_usdhc middleware_fatfs
)
```
### Закрытые решения (Этап 4)
### Закрытые решения — Этап 4
- BSP-слой: `bsp_sd` (host init/deinit/card detect) + `firmware_test_fatfs` (FatFS).
`bsp_usd` как отдельный модуль не создавался — тест работает напрямую через `bsp_sd` + `ff.h`.
@ -133,48 +78,43 @@ target_link_libraries(bsp_usd
- Паттерн: `byte[i] = i & 0xFF`, 4096 байт.
- `critical = false`: тест не блокирует HIL-тесты при отсутствии карты.
- Confirm timeout: 30 000 мс (`PROTOCOL_CONFIRM_TIMEOUT_MS`).
- `SD_HostInit` не вызывается в `bsp_sd_init()``sd_disk_initialize` делает полный init. Двойной init даёт `FR_NOT_READY`.
- Отдельный `test_usd.h` не создавался — `extern K_TEST_USD` объявлен в `test_runner.c`.
---
## Этап 5 — Display + Button (интерактивные)
### test_display
| Параметр | Значение |
| -------- | ------------------------------------ |
| Critical | ❌ |
| HIL | ❌ |
| Confirm | Внутри `run()` — 4 отдельных confirm |
Шаги: заливка Red → confirm → Green → confirm → Blue → confirm → White → confirm.
Каждый шаг посылает `confirm_request`, ждёт `confirm` с таймаутом 15 с.
Итог = AND всех четырёх подтверждений.
`detail` при FAIL содержит ID первого непрошедшего шага: `"display_blue not confirmed"`.
### test_buttons
| Параметр | Значение |
| -------- | ------------------------------------- |
| Critical | ❌ |
| HIL | ❌ |
| Confirm | prompt only (детект через bsp_button) |
Шаги: Test_But_1 → Test_But_2. Таргет посылает `confirm_request` как инструкцию
оператору, детектирует нажатие через `bsp_button` — JSON confirm не нужен.
Таймаут 10 с на каждую кнопку.
## Этап 5 — Display + Buttons ✅
### Аппаратный контекст кнопок
| Кнопка | Пин MCU | GPIO |
| ---------- | ---------- | --------- |
| Test_But_1 | GPIO_B1_14 | GPIO2[30] |
| Test_But_2 | GPIO_B1_15 | GPIO2[31] |
| Кнопка | Пин MCU | GPIO | Схема | Нажатие |
| ---------- | ---------- | --------- | ----------------------------- | ------- |
| Test_But_1 | GPIO_B1_14 | GPIO2[30] | SWT6x6, pull-up к 3V3 внешний | LOW |
| Test_But_2 | GPIO_B1_15 | GPIO2[31] | SWT6x6, pull-up к 3V3 внешний | LOW |
### Закрытые решения — test_display
- `pre_confirm_prompt = NULL` — нет pre-confirm, `test_begin` отправляется сразу.
- 6 шагов confirm: 4 цвета (Red/Green/Blue/White) + 2 ротации (только для TFT ≠ TFT4).
- Таймаут каждого confirm: 15 000 мс. Не подтверждён → FAIL с `detail = "<id> not confirmed"`.
- Ротация: `ROTATE_0` + `FLIP_HORIZONTAL`. Восстановить `ROTATE_0` в любом исходе.
- Фреймбуфер: статический в NonCacheable SDRAM (`AT_NONCACHEABLE_SECTION_ALIGN`, 64-byte align).
- Тип дисплея: `DISPLAY_TEST_TYPE=BSP_DISPLAY_TFT8` через CMake compile definition.
### Закрытые решения — test_buttons
- `pre_confirm_prompt = NULL``confirm_request` используется только как UI-подсказка оператору.
- Хост **не** отправляет `{"type":"confirm",...}`. Детект нажатия — через `bsp_button_get_event_pressed()`.
- Таймаут: 10 000 мс → `TEST_STATUS_SKIP` (не FAIL).
- Порядок: But_1 → But_2.
- `bsp_button_poll()` вызывается каждые 5 мс через rate-limiting по `bsp_tick_get_ms()`.
- На каждом poll дренируются события **обеих** кнопок: предотвращает stale-событие от нецелевой кнопки.
- `bsp_button_init()` вызывается в `init()` тест-модуля — сброс debounce-счётчиков перед тестом.
- `bsp_tick_delay_ms()` не используется — polling pattern аналогичен `test_runner_wait_confirm()`.
---
## Этап 6 — CAN + UART + Opto (HIL, M5StampPLC)
## Этап 6 — CAN + UART + Opto (HIL, M5StampPLC) ← ТЕКУЩИЙ
Все три теста `requires_hil = true`. Запускаются только при наличии стенда.
BSP для всех трёх уже готов.
@ -184,9 +124,10 @@ BSP для всех трёх уже готов.
M5StampPLC отправляет CAN-фрейм → плата принимает → сравниваем ID и payload.
**Шаги:**
1. M5 → `{"cmd":"can_send","id":0x100,"data":[0xDE,0xAD,0xBE,0xEF]}` (через `confirm_request`)
2. Таргет: `uart_cmd("CAN_RECV 500")``"100 DEADBEEF"` или `"TIMEOUT"`
3. Ответный: таргет посылает → M5 `can_recv` → верификация
1. Таргет посылает `confirm_request` → M5 получает команду `can_send`
2. M5 → `{"cmd":"can_send","id":0x100,"data":[0xDE,0xAD,0xBE,0xEF]}`
3. Таргет: ожидает CAN-фрейм, 500 мс → верификация ID и payload
4. Ответный: таргет посылает → M5 `can_recv` → верификация
### test_uart_ttl
@ -202,6 +143,7 @@ M5 RLY2 → RS_RX оптовход (BSP_OPTO_CH_RS) → детект ACTIVE/INAC
M5 RLY3/RLY4 → EXT_IN1/IN2 → детект ACTIVE/INACTIVE через `bsp_opto`.
**Параметры стенда (из HIL_BENCH.md):**
```
RLY2 → RS_RX (BSP_OPTO_CH_RS) GPIO1[23]
RLY3 → EXT_IN1 (BSP_OPTO_CH_IN1) GPIO1[22]
@ -221,7 +163,7 @@ tools/hil/
```
Фикстура `firmware_cdc` открывает CDC порт firmware_test (прошит в Flash),
посылает JSON команды, читает события. Аналог `uart_cmd` для USB CDC.
посылает JSON команды, читает события.
---
@ -241,54 +183,50 @@ tools/hil/
bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); /* 8 байт из OCOTP */
```
### Открытые вопросы
### Открытые вопросы — Этап 7
- [ ] Что именно записывать как "пройдено": флаг в Flash или только отправить UID?
- [ ] Что именно записывать как «пройдено»: флаг в Flash или только отправить UID?
- [ ] Нужна ли защита от повторного provisioning (write-once)?
---
## Матрица тестов — итоговая
## Матрица тестов — итоговая
| ID | Название | Тип | Critical | HIL (M5) | BSP | Статус |
| ---------- | -------------- | ----------- | -------- | -------- | ----------------- | ------ |
| — | PING | cmd | — | ❌ | — | ✅ |
| `sdram` | SDRAM 32MB | self | ✅ | ❌ | `bsp_sdram` ✅ | ✅ |
| `qspi` | QSPI Flash | self | ✅ | ❌ | `bsp_qspi_flash`✅ | ✅ |
| `usd` | uSD (SDIO) | interactive | ❌ | ❌ | `bsp_sd` ✅ | ✅ |
| `display` | Display RGB888 | interactive | ❌ | ❌ | существующий BSP | ⬜ |
| `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | `bsp_button`| ⬜ |
| `can` | CAN loopback | HIL | ❌ | ✅ | `bsp_can` ✅ | ⬜ |
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | `bsp_uart_host`✅ | ⬜ |
| `uart_iso` | UART ISO +24V | HIL | ❌ | ✅ | `bsp_opto` ✅ | ⬜ |
| `opto` | Opto-in EXT | HIL | ❌ | ✅ | `bsp_opto` ✅ | ⬜ |
| ID | Название | Тип | Critical | HIL (M5) | BSP | Статус |
| ---------- | -------------- | ----------- | -------- | -------- | ------------------ | ------ |
| — | PING | cmd | — | ❌ | — | ✅ |
| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | `bsp_sdram` | ✅ |
| `qspi` | QSPI Flash | self | ✅ | ❌ | `bsp_qspi_flash` ✅ | ✅ |
| `usd` | uSD (SDIO) | interactive | ❌ | ❌ | `bsp_sd` | ✅ |
| `display` | Display RGB888 | interactive | ❌ | ❌ | `bsp_display` ✅ | ✅ |
| `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | `bsp_button` | ✅ |
| `can` | CAN loopback | HIL | ❌ | ✅ | `bsp_can` | ⬜ |
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | `bsp_uart_host` ✅ | ⬜ |
| `uart_iso` | UART ISO +24V | HIL | ❌ | ✅ | `bsp_opto` | ⬜ |
| `opto` | Opto-in EXT | HIL | ❌ | ✅ | `bsp_opto` | ⬜ |
---
## Зависимости между этапами
## Зависимости между этапами
```
✅ Этап 1 (протокол v2 + runner)
✅ Этап 2 (bsp_sdram + test_sdram)
✅ Этап 3 (bsp_qspi_flash + test_qspi)
✅ Этап 4 (bsp_sd + test_usd)
⬜ Этап 5 (display + buttons) ← ТЕКУЩИЙ
⬜ Этап 6 (CAN + UART + Opto, HIL)
✅ Этап 5 (display + buttons)
⬜ Этап 6 (CAN + UART + Opto, HIL) ← ТЕКУЩИЙ
⬜ Этап 7 (provisioning)
⬜ Этап 8 (tools/production/ TUI runner) ← параллельно с 6-7
⬜ Этап 8 (tools/production/ TUI runner) ← параллельно с 6-7
```
---
## Хостовое ПО производственного прогона (Этап 8)
**Решение принято (Вариант D):** отдельное приложение `tools/production/`,
без pytest, с TUI (Textual).
**Решение принято (Вариант D):** отдельное приложение `tools/production/`, без pytest, с TUI (Textual).
Подробная архитектура описана в предыдущей версии плана (v0.2, раздел
"Открытый вопрос: ПО на стороне хоста").
### Открытые вопросы (перед Этапом 8)
### Открытые вопросы — Этап 8
- [ ] TUI: Textual или Rich или plain print на первой итерации?
- [ ] БД: SQLite локально или REST API?

View file

@ -29,13 +29,11 @@ extern const test_module_t K_TEST_SDRAM;
extern const test_module_t K_TEST_QSPI;
extern const test_module_t K_TEST_USD;
extern const test_module_t K_TEST_DISPLAY;
extern const test_module_t K_TEST_BUTTONS;
static const test_module_t *const k_registry[] = {
/* populated starting from Этап 2 */
&K_TEST_SDRAM,
&K_TEST_QSPI,
&K_TEST_USD,
&K_TEST_DISPLAY,
&K_TEST_SDRAM, &K_TEST_QSPI, &K_TEST_USD, &K_TEST_DISPLAY, &K_TEST_BUTTONS,
};
#define REGISTRY_SIZE (sizeof(k_registry) / sizeof(k_registry[0]))

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,175 @@
/**
* @file test_buttons.c
* @brief Тест-модуль firmware_test: тактовые кнопки Test_But_1 / Test_But_2.
*
* Логика теста:
* 1. Отправить confirm_request("btn1_press") как инструкцию оператору.
* 2. Поллить bsp_button ждать события нажатия Test_But_1,
* таймаут BTN_PRESS_TIMEOUT_MS SKIP.
* 3. То же для Test_But_2 ("btn2_press").
*
* Хост НЕ отправляет {"type":"confirm",...}.
* confirm_request только UI-подсказка оператору.
* Физическое событие детектируется через bsp_button_get_event_pressed().
*
* Аппаратура:
* Test_But_1 GPIO_B1_14 / GPIO2[30], pull-up 3V3, нажатие = LOW
* Test_But_2 GPIO_B1_15 / GPIO2[31], pull-up 3V3, нажатие = LOW
*/
#include "bsp/button.h"
#include "bsp/tick.h"
#include "bsp/usb_cdc.h"
#include "cli.h"
#include "protocol.h"
#include "test_module.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
/* ── Константы ─────────────────────────────────────────────────────────── */
/** @brief Таймаут ожидания нажатия кнопки, мс. */
#define BTN_PRESS_TIMEOUT_MS 10000U
/** @brief Период вызова bsp_button_poll(), мс. */
#define BTN_POLL_PERIOD_MS 5U
/* ── Вспомогательные функции ────────────────────────────────────────────── */
/**
* @brief Создать результат FAIL с текстовым описанием.
*
* @param p_detail Строка описания (копируется в result.detail).
* @return Заполненный test_result_t со статусом TEST_STATUS_FAIL.
*/
static test_result_t make_fail(const char *p_detail)
{
test_result_t result = { .status = TEST_STATUS_FAIL, .duration_ms = 0U };
(void) snprintf(result.detail, TEST_DETAIL_SIZE, "%s", p_detail);
return result;
}
/**
* @brief Создать результат SKIP с текстовым описанием.
*
* @param p_detail Строка описания (копируется в result.detail).
* @return Заполненный test_result_t со статусом TEST_STATUS_SKIP.
*/
static test_result_t make_skip(const char *p_detail)
{
test_result_t result = { .status = TEST_STATUS_SKIP, .duration_ms = 0U };
(void) snprintf(result.detail, TEST_DETAIL_SIZE, "%s", p_detail);
return result;
}
/**
* @brief Ждать нажатия кнопки с debounce-поллингом.
*
* Отправляет confirm_request как инструкцию оператору хост не отвечает
* JSON-confirm. Физическое нажатие детектируется через bsp_button.
* USB CDC поллится в каждой итерации; bsp_button_poll() вызывается
* каждые BTN_POLL_PERIOD_MS мс через rate-limiting по bsp_tick_get_ms().
*
* @param btn Кнопка для ожидания.
* @param p_id Идентификатор confirm_request.
* @param p_prompt Инструкция оператору.
* @param timeout_ms Таймаут ожидания, мс.
* @return true если нажатие зафиксировано до таймаута, false при таймауте.
*/
static bool wait_button_press(bsp_button_t btn, const char *p_id, const char *p_prompt,
uint32_t timeout_ms)
{
const confirm_params_t params = {
.id = p_id,
.prompt = p_prompt,
.timeout_ms = timeout_ms,
};
protocol_send_confirm_request(&params);
const uint32_t DEADLINE = bsp_tick_get_ms() + timeout_ms;
uint32_t next_poll_ms = bsp_tick_get_ms();
while (bsp_tick_get_ms() < DEADLINE)
{
bsp_usb_cdc_poll();
cli_process();
if (bsp_tick_get_ms() >= next_poll_ms)
{
bsp_button_poll();
next_poll_ms += BTN_POLL_PERIOD_MS;
/* Дренировать события обеих кнопок сразу после poll.
* Нажатие не-целевой кнопки отбрасывается здесь же
* иначе stale-событие засчитается в следующем вызове. */
bool ev1 = bsp_button_get_event_pressed(BSP_BUTTON_1);
bool ev2 = bsp_button_get_event_pressed(BSP_BUTTON_2);
if ((btn == BSP_BUTTON_1 && ev1) || (btn == BSP_BUTTON_2 && ev2))
{
return true;
}
}
}
return false;
}
/* ── Реализация тест-модуля ─────────────────────────────────────────────── */
/**
* @brief Инициализация: сброс debounce-состояния перед тестом.
*
* GPIO уже настроен в BOARD_InitPins(). bsp_button_init() только
* сбрасывает счётчики и флаги событий не трогает железо.
* Гарантирует, что удержание кнопки до старта теста не даёт
* ложного события: get_event_pressed() срабатывает только на переход.
*/
static void buttons_test_init(void)
{
bsp_button_init();
}
/**
* @brief Выполнить тест кнопок.
*
* Шаг 1: ждать нажатия Test_But_1.
* Шаг 2: ждать нажатия Test_But_2.
* Таймаут любого шага SKIP.
*
* @return test_result_t с итогом теста.
*/
static test_result_t buttons_test_run(void)
{
if (!wait_button_press(BSP_BUTTON_1, "btn1_press", "Press Test_But_1", BTN_PRESS_TIMEOUT_MS))
{
return make_skip("btn1_press timeout");
}
if (!wait_button_press(BSP_BUTTON_2, "btn2_press", "Press Test_But_2", BTN_PRESS_TIMEOUT_MS))
{
return make_skip("btn2_press timeout");
}
return (test_result_t){
.status = TEST_STATUS_PASS,
.duration_ms = 0U,
.detail = { 0 },
};
}
/* ── Дескриптор модуля ──────────────────────────────────────────────────── */
/** @brief Дескриптор тест-модуля кнопок для реестра test_runner. */
const test_module_t K_TEST_BUTTONS = {
.id = "buttons",
.name = "Test Buttons",
.critical = false,
.requires_hil = false,
.pre_confirm_prompt = NULL,
.init = buttons_test_init,
.run = buttons_test_run,
.deinit = NULL,
};

View file

@ -1,34 +1,39 @@
# port/ — porting layer
Glue-код между сторонними библиотеками (`lib/`) и платформой (`bsp/`).
Glue-код между сторонними библиотеками (`lib/`, `utils/`) и платформой (`bsp/`).
---
## Зачем нужен отдельный каталог
## Концепция
В проекте три слоя кода с чёткими границами:
В проекте четыре слоя с чёткими границами:
```bash
lib/ сторонний код — ничего не знает о проекте
bsp/ железо — драйверы периферии MIMXRT1052
utils/ платформонезависимые алгоритмы (ring_buffer, log и др.)
port/ ← glue: адаптирует lib/ и utils/ к конкретной платформе
firmware/ бизнес-логика — использует всё вышеперечисленное
```mermaid
graph TB
FW["firmware/*\nбизнес-логика"]
PORT["port/\nglue: адаптирует utils/ и lib/ к платформе"]
BSP["bsp/\nдрайверы периферии MIMXRT1052"]
UTILS["utils/\nплатформонезависимые алгоритмы"]
LIB["lib/ + sdk/\nсторонний код"]
FW --> PORT
FW --> BSP
FW --> UTILS
PORT --> BSP
PORT --> UTILS
PORT --> LIB
```
Код попадает в `port/` если выполняются оба условия:
1. Связывает платформонезависимую библиотеку/утилиту с конкретным BSP.
1. Связывает платформонезависимую библиотеку / утилиту с конкретным BSP.
2. Сам по себе не является ни библиотекой, ни драйвером.
Примеры: адаптер логгера к UART, diskio-реализация FatFS поверх bsp_sdio,
FreeRTOS heap и assert-хуки.
Код НЕ попадает в `port/` если:
**Не попадает в `port/`:**
- Не зависит от `bsp/` → идёт в `utils/`
- Является самостоятельным драйвером периферии → идёт в `bsp/`
- Это сторонняя библиотека без изменений → идёт в `lib/`
- Самостоятельный драйвер периферии → идёт в `bsp/`
- Сторонняя библиотека без изменений → идёт в `lib/`
---
@ -38,12 +43,35 @@ FreeRTOS heap и assert-хуки.
port/
├── CMakeLists.txt
├── README.md ← этот файл
└── log/ ← UART-адаптер для utils/log
├── fatfs/ ← diskio поверх bsp_sd / bsp_qspi
# Планируется:
└── freertos/ ← heap_4.c, configASSERT, vApplicationHooks
├── log/ ← UART-адаптер для utils/log
│ ├── CMakeLists.txt # таргет port_log_uart
│ ├── README.md
│ ├── include/port/
│ │ └── log_uart.h
│ └── src/
│ └── log_uart.c
└── fatfs/ ← diskio поверх bsp_sd (FatFS)
├── CMakeLists.txt # таргет port_fatfs_sd (INTERFACE)
└── sd/
├── CMakeLists.txt
├── include/port/fatfs/
│ └── diskio_sd.h
└── src/
└── diskio_sd.c
```
**Планируется:** `port/freertos/``heap_4.c`, `configASSERT`,
`vApplicationHooks`.
---
## Таблица адаптеров
| Таргет CMake | Что адаптирует | Куда | Тип |
| --------------- | -------------- | --------------- | --------- |
| `port_log_uart` | `utils/log` | `bsp_uart_host` | STATIC |
| `port_fatfs_sd` | FatFS diskio | `bsp_sd` | INTERFACE |
---
## Соглашения
@ -51,8 +79,8 @@ port/
**Именование таргетов:** `port_<что>_<транспорт>` — например `port_log_uart`,
`port_fatfs_sd`. Позволяет иметь несколько адаптеров для одной библиотеки.
**Include-путь:** `#include "port/<модуль>.h"`публичные заголовки
всегда в `port/<модуль>/include/port/`.
**Include-путь:** публичные заголовки в `port/<модуль>/include/port/`,
подключение через `#include "port/<модуль>.h"`.
**Не компилируется для host-тестов:** `bsp/` недоступен на хосте, поэтому
`port/CMakeLists.txt` возвращает управление при `BUILD_TESTS_HOST=ON`.
**Host-сборка:** `port/CMakeLists.txt` содержит ранний `return()` при
`BUILD_TESTS_HOST=ON` — `bsp/` недоступен на хосте.

89
port/fatfs/README.md Normal file
View file

@ -0,0 +1,89 @@
# port/fatfs — FatFS diskio поверх bsp_sd
Адаптирует FatFS diskio-интерфейс к `bsp_sd` через NXP `fsl_sd_disk`.
Предоставляет тонкую обёртку `microsd_disk_*``sd_disk_*` и изолирует
номер диска от вызывающего кода.
---
## Архитектура
```mermaid
graph TB
FW["firmware_test / tft_app\nf_mount / f_read / f_write"]
DISKIO["diskio.c\nв каждом бинарнике\n(диспетчер по pdrv)"]
PORT["port/fatfs/sd\nmicrosd_disk_*"]
FSL["fsl_sd_disk\nsd_disk_* (NXP SDK)"]
BSP["bsp_sd\nSD_HostInit / SD_Read / SD_Write"]
HW["USDHC1 / SD карта"]
FW --> DISKIO --> PORT --> FSL --> BSP --> HW
```
**Почему `diskio_sd.c` не компилируется самим `port_fatfs_sd`:**
`diskio_sd.c` включает `diskio.h``ff.h``ffconf.h`. Этот файл
конфигурации разный у `firmware_test` (bare-metal, `FF_FS_REENTRANT=0`)
и `tft_app` (FreeRTOS, `FF_FS_REENTRANT=1`). Поэтому `port_fatfs_sd`
INTERFACE-библиотека: предоставляет include path и зависимости, а каждый
бинарник компилирует `diskio_sd.c` в свой FatFS-таргет самостоятельно
через переменную `PORT_FATFS_SD_SRC`.
---
## Использование в бинарнике
```cmake
# firmware/test/CMakeLists.txt
add_library(firmware_test_fatfs STATIC
fatfs/ff.c
fatfs/diskio.c
${PORT_FATFS_SD_SRC} # ← diskio_sd.c с правильным ffconf.h
)
target_link_libraries(firmware_test_fatfs
PRIVATE port_fatfs_sd # include path + bsp_sd + sdk_fatfs_headers
)
```
```c
#include "port/fatfs/diskio_sd.h"
/* diskio.c вызывает microsd_disk_* через диспетчер по pdrv: */
DSTATUS disk_initialize(BYTE pdrv)
{
if (pdrv == SDDISK) return microsd_disk_initialize(pdrv);
return STA_NOINIT;
}
```
---
## API (`microsd_disk_*`)
```c
DSTATUS microsd_disk_initialize(BYTE pdrv);
DSTATUS microsd_disk_status(BYTE pdrv);
DRESULT microsd_disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count);
DRESULT microsd_disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count);
DRESULT microsd_disk_ioctl(BYTE pdrv, BYTE cmd, void *buff);
```
Функции — тонкие обёртки над `sd_disk_*` из NXP SDK. Не вызывать из
прикладного кода напрямую — только через диспетчер `diskio.c`.
---
## CMake
```cmake
# Получить путь к diskio_sd.c
target_link_libraries(<fatfs_target> PRIVATE port_fatfs_sd)
# PORT_FATFS_SD_SRC автоматически доступна после add_subdirectory(port)
```
**Зависимости модуля:**
| Зависимость | Тип | Описание |
| ------------------- | --------- | ------------------------------------------ |
| `sdk_fatfs_headers` | INTERFACE | `ff.h`, `fsl_sd_disk.h`, `SDK_FATFS_*_SRC` |
| `bsp_sd` | INTERFACE | `g_sd`, `bsp_sd_is_inserted()` |

View file

@ -6,7 +6,40 @@
---
## Использование
## Архитектура
```mermaid
flowchart LR
APP["LOG_I / LOG_D / …\nлюбой .c файл"]
CORE["utils/log\nlog_write()"]
ADAPTER["port_log_uart\nuart_write callback\nlog_get_timestamp_ms()"]
BSP_U["bsp_uart_host\nbsp_uart_host_write()"]
BSP_T["bsp_tick\nbsp_tick_get_ms()"]
APP --> CORE --> ADAPTER
ADAPTER --> BSP_U
ADAPTER --> BSP_T
```
`log_uart_init()` делает две вещи:
1. Регистрирует `bsp_uart_host_write()` как write callback через `log_init()`.
2. Предоставляет strong-реализацию weak-хука `log_get_timestamp_ms()`
`bsp_tick_get_ms()`.
---
## API
```c
void log_uart_init(void);
```
Предусловия: `bsp_uart_host_init()` и `bsp_tick_init()` уже вызваны.
---
## Быстрый старт
```c
#include "port/log_uart.h"
@ -18,27 +51,38 @@ int main(void)
bsp_tick_init();
bsp_uart_host_init(115200U);
log_uart_init(); // регистрирует транспорт, после этого LOG_* работают
log_uart_init(); /* регистрирует транспорт и timestamp */
LOG_I("BOOT", "Ready");
}
```
Для **tft_app (FreeRTOS)** — добавить мьютекс до `log_uart_init()`:
```c
log_mutex_init(); // создать FreeRTOS-семафор
log_uart_init(); // зарегистрировать транспорт
```
Реализация мьютекса: `firmware/tft_app/src/log_mutex.c`.
---
## Что делает `log_uart_init()`
## Интеграция с FreeRTOS (`tft_app`)
1. Регистрирует `bsp_uart_host_write()` как write callback через `log_init()`.
2. Предоставляет strong-реализацию `log_get_timestamp_ms()``bsp_tick_get_ms()`.
Для потокобезопасности создать мьютекс **до** `log_uart_init()`:
```c
log_mutex_init(); /* создать FreeRTOS-семафор */
log_uart_init(); /* зарегистрировать транспорт */
```
Реализация мьютекса — в `firmware/tft_app/src/log_mutex.c`:
```c
#include "FreeRTOS.h"
#include "log/log.h"
#include "semphr.h"
static SemaphoreHandle_t s_log_mutex;
void log_mutex_init(void) { s_log_mutex = xSemaphoreCreateMutex(); }
void log_mutex_lock(void) { xSemaphoreTake(s_log_mutex, portMAX_DELAY); }
void log_mutex_unlock(void) { xSemaphoreGive(s_log_mutex); }
```
> `LOG_*` нельзя вызывать из ISR — `bsp_uart_host_write()` блокирующий.
---
@ -48,4 +92,13 @@ log_uart_init(); // зарегистрировать транспорт
target_link_libraries(<target> PRIVATE port_log_uart)
```
Транзитивно подтягивает `utils` (содержит `log.h`) и `LOG_LEVEL`.
Транзитивно подтягивает `utils` (содержит `log.h` и `LOG_LEVEL`),
`bsp_uart_host` и `bsp_tick`.
**Зависимости модуля:**
| Зависимость | Тип | Описание |
| --------------- | ------ | -------------------------------- |
| `utils` | PUBLIC | `log.h`, `LOG_LEVEL` транзитивно |
| `bsp_uart_host` | PUBLIC | write callback |
| `bsp_tick` | PUBLIC | `log_get_timestamp_ms()` |

View file

@ -1,300 +0,0 @@
.
├── bootstrap.sh
├── bsp
│   ├── button
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   └── src
│   ├── can
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── mocks
│   │   ├── README.md
│   │   └── src
│   ├── CMakeLists.txt
│   ├── common
│   │   ├── CMakeLists.txt
│   │   └── include
│   ├── display
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   └── src
│   ├── generated
│   │   ├── board
│   │   ├── board.c
│   │   ├── board.h
│   │   ├── clock_config.c
│   │   ├── clock_config.h
│   │   ├── pin_mux.c
│   │   ├── pin_mux.h
│   │   ├── README.md
│   │   ├── startup
│   │   ├── syscalls.c
│   │   └── TFT_Board.mex
│   ├── led
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   └── src
│   ├── opto
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   └── src
│   ├── qspi_flash
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   ├── REFACTORING.md
│   │   └── src
│   ├── README.md
│   ├── sdram
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   └── src
│   ├── tick
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   └── src
│   ├── uart_host
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── mocks
│   │   ├── README.md
│   │   └── src
│   └── usb_cdc
│   ├── CMakeLists.txt
│   ├── include
│   ├── README.md
│   └── src
├── cmake
│   ├── linker
│   │   ├── MIMXRT1052xxxxx_flexspi_nor_sdram.ld
│   │   ├── MIMXRT1052xxxxx_flexspi_nor.ld
│   │   ├── MIMXRT1052xxxxx_ram.ld
│   │   ├── MIMXRT1052xxxxx_sdram_txt.ld
│   │   └── MIMXRT1052xxxxx_sdram.ld
│   ├── toolchain_arm.cmake
│   └── toolchain_host.cmake
├── CMakeLists.txt
├── CMakePresets.json
├── docs
│   ├── DEV_ARCH.md
│   ├── hardware
│   │   ├── board
│   │   ├── m5stamPLC
│   │   ├── MCULINKINSERT.pdf
│   │   └── tft_displays
│   ├── HOW_TO_DEBUG.md
│   ├── HOW_TO_FLASH.md
│   ├── mimxrt1052
│   │   ├── BOOT_FLAGS.md
│   │   ├── HAB_GUIDE.md
│   │   ├── manufacturing_user's_guide.pdf
│   │   └── mcu_rm.pdf
│   └── testing
│   ├── hil
│   ├── host
│   └── PROTOCOL.md
├── firmware
│   ├── bootloader
│   ├── test
│   │   ├── CMakeLists.txt
│   │   ├── PLAN.md
│   │   ├── README.md
│   │   └── src
│   └── tft_app
├── just
│   ├── build.just
│   ├── ci_workflow.md
│   ├── ci.just
│   └── host.just
├── Justfile
├── lib
│   ├── CMakeLists.txt
│   ├── fff
│   │   ├── buildandtest
│   │   ├── CMakeLists.txt
│   │   ├── examples
│   │   ├── fakegen.rb
│   │   ├── fff.h
│   │   ├── LICENSE
│   │   ├── README.md
│   │   └── test
│   ├── SEGGER
│   │   ├── RTT
│   │   └── wrapper
│   └── Unity
│   ├── auto
│   ├── CMakeLists.txt
│   ├── docs
│   ├── examples
│   ├── extras
│   ├── library.json
│   ├── LICENSE.txt
│   ├── meson_options.txt
│   ├── meson.build
│   ├── platformio-build.py
│   ├── README.md
│   ├── src
│   ├── test
│   └── unityConfig.cmake
├── port
│   ├── CMakeLists.txt
│   ├── log
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   ├── README.md
│   │   └── src
│   └── README.md
├── project_tree.txt
├── pyocd_debug.yaml
├── pyocd.yaml
├── README.md
├── sdk
│   ├── boards
│   │   └── evkbimxrt1050
│   ├── CMakeLists.txt
│   ├── CMSIS
│   │   ├── Core
│   │   ├── Driver
│   │   ├── DSP
│   │   ├── LICENSE.txt
│   │   ├── NN
│   │   └── RTOS2
│   ├── components
│   │   ├── audio
│   │   ├── button
│   │   ├── codec
│   │   ├── common_task
│   │   ├── crc
│   │   ├── display
│   │   ├── exception_handling
│   │   ├── flash
│   │   ├── gpio
│   │   ├── i2c
│   │   ├── internal_flash
│   │   ├── led
│   │   ├── lists
│   │   ├── log
│   │   ├── mem_manager
│   │   ├── messaging
│   │   ├── mx25r_flash
│   │   ├── osa
│   │   ├── panic
│   │   ├── phy
│   │   ├── pmic
│   │   ├── reset
│   │   ├── rng
│   │   ├── rtt
│   │   ├── sensor
│   │   ├── serial_manager
│   │   ├── silicon_id
│   │   ├── spi
│   │   ├── timer
│   │   ├── timer_manager
│   │   ├── touch
│   │   ├── uart
│   │   ├── video
│   │   └── wifi_bt_module
│   ├── COPYING-BSD-3
│   ├── devices
│   │   └── MIMXRT1052
│   ├── docs
│   │   └── readme.md
│   ├── LA_OPT_NXP_Software_License.txt
│   ├── middleware
│   │   ├── audio_voice
│   │   ├── cjson
│   │   ├── dhara
│   │   ├── fatfs
│   │   ├── freemaster
│   │   ├── libjpeg
│   │   ├── littlefs
│   │   ├── mcuboot_opensource
│   │   ├── pkcs11
│   │   ├── pngdec
│   │   ├── sdmmc
│   │   ├── tfm
│   │   ├── tinycbor
│   │   └── usb
│   ├── MIMXRT1052xxxxB_manifest_v3_15.xml
│   ├── README.md
│   ├── rtos
│   │   └── freertos
│   ├── sdk_tree.txt
│   ├── SW-Content-Register.txt
│   └── tools
│   └── cmake_toolchain_files
├── tests
│   ├── CMakeLists.txt
│   ├── host
│   │   ├── button
│   │   ├── can
│   │   ├── cli
│   │   ├── CMakeLists.txt
│   │   ├── led
│   │   ├── log
│   │   ├── mocks
│   │   ├── opto
│   │   ├── prio_queue
│   │   ├── protocol
│   │   ├── README.md
│   │   ├── ring_buffer
│   │   ├── runner
│   │   ├── timeout
│   │   └── uart_host
│   └── target
│   ├── CMakeLists.txt
│   ├── hil_button
│   ├── hil_can
│   ├── hil_opto
│   ├── hil_usb_cdc
│   └── host_uart
├── tools
│   ├── hil
│   │   ├── __pycache__
│   │   ├── 01_test_uart.py
│   │   ├── 02_test_opto.py
│   │   ├── 03_test_can.py
│   │   ├── 04_test_button.py
│   │   ├── 05_test_usb_cdc.py
│   │   ├── conftest.py
│   │   ├── env_config.py
│   │   ├── load_and_run.py
│   │   ├── m5
│   │   ├── pyocd_utils.py
│   │   ├── pyproject.toml
│   │   ├── README.md
│   │   └── uv.lock
│   ├── host
│   │   ├── dcd
│   │   ├── flash_swd.py
│   │   ├── flash_usb.py
│   │   ├── hab
│   │   ├── pyproject.toml
│   │   ├── README.md
│   │   └── uv.lock
│   ├── production
│   └── shared
└── utils
├── CMakeLists.txt
├── log
│   ├── log.c
│   ├── log.h
│   └── README.md
├── prio_queue
│   ├── prio_queue.c
│   ├── prio_queue.h
│   └── README.md
├── README.md
└── ring_buffer
├── README.md
├── ring_buffer.c
└── ring_buffer.h
174 directories, 124 files

View file

@ -1,8 +1,7 @@
# utils
# utils — платформонезависимые утилиты
Платформонезависимые утилиты проекта.
**Правило включения** — код попадает сюда только если выполняются оба условия:
Общие структуры данных и сервисы проекта. Код попадает сюда только если
выполняются оба условия:
- не зависит от железа (нет `fsl_*`, CMSIS, FreeRTOS, BSP);
- используется более чем в одном месте проекта.
@ -13,7 +12,36 @@
## Модули
| Модуль | Путь | Описание |
| ------------- | ------------------------------------- | -------------------------------------------------- |
| `ring_buffer` | [ring_buffer/](ring_buffer/README.md) | SPSC кольцевой буфер байт, lock-free |
| `log` | [log/](log/README.md) | Платформонезависимый логгер с callback-транспортом |
| Модуль | Путь | Описание |
| ------------- | ---------------------------------------------- | ------------------------------------ |
| `ring_buffer` | [ring_buffer/README.md](ring_buffer/README.md) | SPSC кольцевой буфер байт, lock-free |
| `prio_queue` | [prio_queue/README.md](prio_queue/README.md) | Приоритетная очередь с вытеснением |
| `log` | [log/README.md](log/README.md) | Логгер с callback-транспортом |
---
## CMake
Все три модуля собираются в одну статическую библиотеку `utils`:
```cmake
target_link_libraries(<target> PRIVATE utils)
```
Корень `utils/` автоматически добавляется в include path — `#include`
указывает полный путь от корня:
```c
#include "ring_buffer/ring_buffer.h"
#include "prio_queue/prio_queue.h"
#include "log/log.h"
```
`LOG_LEVEL` пробрасывается из CMake-пресета или командной строки:
```cmake
target_compile_definitions(utils PUBLIC LOG_LEVEL=4)
```
Если не задан — `log.h` выбирает уровень сам через `NDEBUG`
(Verbose в Debug, Off в Release).

View file

@ -1,46 +1,71 @@
# utils/log
# log — платформонезависимый логгер
Платформонезависимый логгер с callback-транспортом.
Ядро логгера (`log.c` / `log.h`) не знает о конкретном транспорте — UART,
USB CDC, Flash и т.д. Транспорт подключается через `log_init()` в виде
callback-функции. Адаптеры живут в `port/log/`.
Ядро логгера (`log.c` / `log.h`) не знает о конкретном транспорте. Транспорт
подключается через `log_init()` в виде callback-функции. Адаптеры живут в
`port/log/`.
| Параметр | Значение |
| ------------------ | ------------------------------------------------- |
| Формат | `[ timestamp][L][TAG] сообщение\r\n` |
| Формат строки | `[ timestamp][L][TAG] сообщение\r\n` |
| Буфер строки | 256 байт (переопределяется через `LOG_BUF_SIZE`) |
| Управление уровнем | `LOG_LEVEL` через CMake `-DLOG_LEVEL=N` |
| Thread-safety | мьютекс через weak-хуки (`log_mutex_lock/unlock`) |
| Зависимости | `<stdarg.h>`, `<stdio.h>`, `<stddef.h>` |
---
## Уровни
| N | Макрос | Имя |
| --- | ------- | -------------------------------------------- |
| 0 | — | off — все `LOG_*``((void)0)`, нулевой ROM |
| 1 | `LOG_E` | error |
| 2 | `LOG_W` | warn |
| 3 | `LOG_I` | info |
| 4 | `LOG_D` | debug |
| 5 | `LOG_V` | verbose |
| N | Макрос | Уровень | Дефолт |
| --- | ------- | ------- | ------------------ |
| 0 | — | off | Release (`NDEBUG`) |
| 1 | `LOG_E` | error | |
| 2 | `LOG_W` | warn | |
| 3 | `LOG_I` | info | |
| 4 | `LOG_D` | debug | |
| 5 | `LOG_V` | verbose | Debug |
По умолчанию: `VERBOSE` в Debug-сборке, `OFF` в Release (`NDEBUG`).
При `LOG_LEVEL=0` макросы разворачиваются в `((void)0)` — нулевой ROM,
`log_write()` не вызывается вообще.
---
## API
```c
void log_init(log_write_cb_t p_write_cb, void *p_ctx);
void log_write(int level, const char *p_tag, const char *p_fmt, ...);
/* Макросы — используй их, не log_write напрямую: */
LOG_E(tag, fmt, ...)
LOG_W(tag, fmt, ...)
LOG_I(tag, fmt, ...)
LOG_D(tag, fmt, ...)
LOG_V(tag, fmt, ...)
```
**Транспортный callback:**
```c
typedef void (*log_write_cb_t)(const char *p_buf, size_t len, void *p_ctx);
```
---
## Быстрый старт
```c
// main.c — зарегистрировать транспорт
/* main.c */
#include "log/log.h"
#include "port/log_uart.h"
log_uart_init(); // инициализировать адаптер транспорта
log_uart_init();
log_init(uart_log_write, NULL);
// Любой .c файл
/* Любой .c файл: */
#include "log/log.h"
LOG_I("BOOT", "Started, tick=%lu", (unsigned long) bsp_tick_get_ms());
LOG_I("BOOT", "Started, tick=%lu", (unsigned long)bsp_tick_get_ms());
LOG_W("SDIO", "Card not detected");
LOG_D("UART", "RX=%u bytes", bsp_uart_host_rx_available());
LOG_E("CAN", "Bus-off, err=%d", err);
@ -53,13 +78,9 @@ LOG_E("CAN", "Bus-off, err=%d", err);
[ 1235][W][SDIO] Card not detected
```
## Транспортный адаптер
---
Адаптер — функция типа `log_write_cb_t`:
```c
typedef void (*log_write_cb_t)(const char *p_buf, size_t len, void *p_ctx);
```
## Транспортные адаптеры
Готовые адаптеры в `port/log/`:
@ -67,14 +88,30 @@ typedef void (*log_write_cb_t)(const char *p_buf, size_t len, void *p_ctx);
| ---------- | ---------------------------------------- |
| `log_uart` | `bsp_uart_host` (LPUART1, MCU-Link VCOM) |
## Мьютекс и временна́я метка (FreeRTOS)
Написать свой — реализовать функцию типа `log_write_cb_t` и передать в
`log_init()`.
Для bare-metal ничего делать не нужно — weak-хуки по умолчанию NOP, временна́я метка возвращает 0.
---
## Тестирование
Host unit-тесты: `tests/host/log/` (Unity + fff).
```bash
just build::test-host
```
---
## Интеграция с FreeRTOS
Для bare-metal ничего делать не нужно — weak-хуки по умолчанию NOP,
временна́я метка возвращает 0.
Для FreeRTOS переопределить в одном `.c` файле прошивки:
```c
// firmware/tft_app/src/log_os.c
/* firmware/tft_app/src/log_os.c */
#include "log/log.h"
#include "FreeRTOS.h"
#include "semphr.h"
@ -89,8 +126,18 @@ void log_mutex_unlock(void) { xSemaphoreGive(s_mutex); }
uint32_t log_get_timestamp_ms(void) { return bsp_tick_get_ms(); }
```
> ⚠️ `LOG_*` нельзя вызывать из ISR если callback транспорта блокирующий.
> `LOG_*` нельзя вызывать из ISR если callback транспорта блокирующий.
## Тесты
---
`tests/host/log/` — host unit-тесты (Unity + fff).
## CMake
```cmake
target_link_libraries(<target> PRIVATE utils)
```
`LOG_LEVEL` задаётся в пресете или явно:
```cmake
target_compile_definitions(utils PUBLIC LOG_LEVEL=4)
```

View file

@ -1,22 +1,45 @@
# util/prio_queue
# prio_queue — приоритетная очередь с вытеснением
Отсортированный массив с фиксированной ёмкостью — generic приоритетная очередь
с вытеснением.
**Типичное использование:** очередь задач, событий или медиадорожек с приоритетами,
где число элементов заранее известно и невелико (≤ ~32). При полном буфере новый
элемент с более высоким приоритетом вытесняет наименее приоритетный.
Отсортированный массив с фиксированной ёмкостью и generic элементами.
При полном буфере новый элемент с более высоким приоритетом вытесняет
наименее приоритетный. Оптимален для небольших очередей (n ≤ 32):
задачи, события, медиадорожки с приоритетами.
| Параметр | Значение |
| ------------- | ----------------------------------------------------------------------- |
| Элемент | любой тип, задаётся через `item_size` |
| Ёмкость | любая, задаётся при `prio_queue_init`; фиксирована на всё время жизни |
| Порядок | определяется `cmp`-функцией пользователя (аналог `qsort`) |
| Вставка | O(n) сдвиг; оптимально при n ≤ 32 |
| Ёмкость | фиксирована на всё время жизни, задаётся при `prio_queue_init` |
| Порядок | определяется `cmp`-функцией пользователя (сигнатура как у `qsort`) |
| Вставка | O(n) сдвиг |
| Peek-top | O(1) |
| Thread-safety | нет; при использовании из нескольких контекстов — внешняя синхронизация |
| Зависимости | `<stdint.h>`, `<stddef.h>`, `<string.h>` |
---
## API
```c
void prio_queue_init(prio_queue_t *q, void *buf, uint8_t capacity,
size_t item_size, pq_cmp_fn cmp);
pq_status_t prio_queue_insert(prio_queue_t *q, const void *item);
const void *prio_queue_peek(const prio_queue_t *q);
uint8_t prio_queue_size(const prio_queue_t *q);
void *prio_queue_at(prio_queue_t *q, uint8_t idx);
void prio_queue_remove_at(prio_queue_t *q, uint8_t idx);
```
**Коды возврата `prio_queue_insert`:**
| Код | Условие |
| ------------ | ----------------------------------------------------- |
| `PQ_OK` | Вставлен, место было |
| `PQ_EVICTED` | Вставлен, наименее приоритетный вытеснен |
| `PQ_FULL` | Отклонён — новый элемент наименее приоритетен из всех |
---
## Быстрый старт
```c
@ -25,43 +48,39 @@
typedef struct { int priority; const char *name; } task_t;
/* Comparator: меньший priority → ближе к голове */
static int task_cmp(const void *a, const void *b) {
static int task_cmp(const void *a, const void *b)
{
int pa = ((const task_t *)a)->priority;
int pb = ((const task_t *)b)->priority;
return (pa < pb) ? -1 : (pa > pb) ? 1 : 0;
}
static task_t storage[8];
static task_t storage[8];
static prio_queue_t q;
/* Инициализация */
prio_queue_init(&q, storage, 8, sizeof(task_t), task_cmp);
/* Вставка */
task_t t = { .priority = 2, .name = "send_data" };
pq_status_t st = prio_queue_insert(&q, &t);
/* st == PQ_OK — вставлен
st == PQ_EVICTED — вставлен, наименее приоритетный вытеснен
st == PQ_FULL — отклонён, новый элемент наименее приоритетен из всех */
/* Чтение верхнего элемента без удаления */
const task_t *top = prio_queue_peek(&q);
if (top != NULL) { /* обработать top */ }
if (top != NULL) { /* обработать */ }
/* Удаление верхнего элемента после обработки */
prio_queue_remove_at(&q, 0);
prio_queue_remove_at(&q, 0); /* удалить верхний после обработки */
```
## Политика вытеснения при полном буфере
**Политика вытеснения при полном буфере:**
```bash
```
Очередь полна [A(1) B(2) C(3)], вставляем D(2):
→ D приоритетнее C(3) → C вытесняется → [A(1) B(2) D(2)] PQ_EVICTED
→ D приоритетнее C(3) → C вытесняется → [A(1) B(2) D(2)] PQ_EVICTED
Очередь полна [A(1) B(2) C(3)], вставляем E(5):
→ E менее приоритетен чем все → отклоняется PQ_FULL
→ E менее приоритетен чем все → отклоняется PQ_FULL
```
---
## Comparator
Сигнатура идентична `qsort`:
@ -76,17 +95,12 @@ typedef int (*pq_cmp_fn)(const void *a, const void *b);
| `> 0` | `b` стоит перед `a` |
| `0` | равнозначны; порядок вставки сохраняется |
## API
---
```c
void prio_queue_init(prio_queue_t *q, void *buf, uint8_t capacity,
size_t item_size, pq_cmp_fn cmp);
## Тестирование
pq_status_t prio_queue_insert(prio_queue_t *q, const void *item);
const void *prio_queue_peek(const prio_queue_t *q);
uint8_t prio_queue_size(const prio_queue_t *q);
void *prio_queue_at(prio_queue_t *q, uint8_t idx);
void prio_queue_remove_at(prio_queue_t *q, uint8_t idx);
Host unit-тесты: `tests/host/prio_queue/`.
```bash
just build::test-host
```
Тесты: `tests/host/test_prio_queue.c`

View file

@ -1,10 +1,8 @@
# util/ring_buffer
# ring_buffer — SPSC кольцевой буфер байт
Кольцевой буфер байт — SPSC (single-producer / single-consumer), lock-free.
**Типичное использование:** ISR пишет принятые байты, задача или основной цикл
читает. Не требует отключения прерываний при условии единственного producer и
единственного consumer.
Lock-free кольцевой буфер для сценария единственный producer / единственный
consumer. Типичное использование: ISR пишет принятые байты, задача или main
loop читает — без отключения прерываний.
| Параметр | Значение |
| ------------- | ------------------------------------------------------------------------------ |
@ -13,23 +11,76 @@
| Thread-safety | SPSC без блокировок; multi-producer/consumer — только с внешней синхронизацией |
| Зависимости | `<stdint.h>`, `<stddef.h>`, `<stdbool.h>` |
---
## API
```c
/* Инициализация */
bool ring_buffer_init(ring_buffer_desc_t *p_desc, uint8_t *p_buf, size_t size);
void ring_buffer_reset(ring_buffer_desc_t *p_desc);
/* Состояние */
bool ring_buffer_is_empty(const ring_buffer_desc_t *p_desc);
bool ring_buffer_is_full(const ring_buffer_desc_t *p_desc);
size_t ring_buffer_count(const ring_buffer_desc_t *p_desc);
size_t ring_buffer_free(const ring_buffer_desc_t *p_desc);
/* Запись (producer) */
bool ring_buffer_put(ring_buffer_desc_t *p_desc, uint8_t byte);
size_t ring_buffer_write(ring_buffer_desc_t *p_desc, const uint8_t *p_data, size_t len);
/* Чтение (consumer) */
bool ring_buffer_get(ring_buffer_desc_t *p_desc, uint8_t *p_byte);
size_t ring_buffer_read(ring_buffer_desc_t *p_desc, uint8_t *p_data, size_t len);
```
`ring_buffer_init` требует `size` — степень двойки; возвращает `false` при
невалидных аргументах. `ring_buffer_write` / `ring_buffer_read` возвращают
фактически переданное количество байт.
---
## Быстрый старт
```c
#include "ring_buffer/ring_buffer.h"
static uint8_t storage[256];
static ring_buffer_t rb;
static uint8_t storage[256]; /* степень двойки */
static ring_buffer_desc_t rb;
// Инициализация (размер — степень двойки)
ring_buffer_init(&rb, storage, sizeof(storage));
// Запись (например, из ISR)
ring_buffer_put(&rb, byte);
/* Producer (например, из ISR): */
ring_buffer_put(&rb, received_byte);
// Чтение (например, из задачи)
/* Consumer (например, из main loop): */
uint8_t b;
if (ring_buffer_get(&rb, &b)) { /* обработать b */ }
if (ring_buffer_get(&rb, &b)) {
/* обработать b */
}
```
Тесты: `tests/host/test_ring_buffer.c` (24 теста, включая wraparound и SPSC-симуляцию).
---
## Тестирование
Host unit-тесты: `tests/host/ring_buffer/` — 24 теста, покрывают wraparound,
граничные значения и SPSC-симуляцию.
```bash
just build::test-host
```
---
## Примечания по реализации
**Wraparound.** Индексы `head` и `tail` — монотонно возрастающие `size_t`.
Маскирование через `& mask` (где `mask = size - 1`) даёт корректный индекс
ячейки. Беззнаковый wraparound арифметически корректен: `(0 - 1) == SIZE_MAX`,
подсчёт заполненности через `tail - head` работает без явной обёртки.
**Memory ordering.** На Cortex-M7 (strongly-ordered) барьер памяти не нужен.
На weakly-ordered архитектурах (ARM64, RISC-V) потребуется store-release /
load-acquire — добавить `__atomic_store_n` / `__atomic_load_n`.