diff --git a/.gitignore b/.gitignore index e8ea310..36a74bd 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,4 @@ __pycache__/ tools/host/.venv-host/ tools/host/.venv-host-win/ .zed/ +project_tree.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 0397732..e2587ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Журнал изменений: `tft_manufacture_test` -Репозиторий: 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..` +Сравнение: ... + +### Кратко + +- 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`. ## Отфильтрованный шум базового среза diff --git a/README.md b/README.md index b2ab416..a79e6d8 100644 --- a/README.md +++ b/README.md @@ -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//`) и pytest-файл (`tools/hil/NN_test_.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-зависимостей). --- diff --git a/bsp/README.md b/bsp/README.md index 7635711..7594132 100644 --- a/bsp/README.md +++ b/bsp/README.md @@ -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 */ diff --git a/bsp/button/README.md b/bsp/button/README.md index 3c21e9c..5ba0516 100644 --- a/bsp/button/README.md +++ b/bsp/button/README.md @@ -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 -около 50–100 мс. +--- + +## 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 (рекомендуется 15–30 мс). - ---- - ## Тестирование ### 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 ` | `1` / `0` | Сырое состояние (без debounce) | -| `STATE ` | `1` / `0` | Стабильное состояние после debounce | +| `READ ` | `1` / `0` | Сырое состояние без дебаунса | +| `STATE ` | `1` / `0` | Стабильное состояние после дебаунса | | `EVENT_P ` | `1` / `0` | `get_event_pressed`, сбрасывает флаг | | `EVENT_R ` | `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()` | diff --git a/bsp/can/README.md b/bsp/can/README.md index dc98ef5..d6e851e 100644 --- a/bsp/can/README.md +++ b/bsp/can/README.md @@ -1,47 +1,93 @@ -# bsp_can +# bsp_can — FlexCAN2 (CAN 2.0) -Приём и передача CAN 2.0 фреймов через FlexCAN2 (трансивер SN65HVD230D, разъём P1 контакты 3–4). +Приём и передача CAN 2.0 фреймов через FlexCAN2 (трансивер SN65HVD230D, +разъём P1 контакты 3–4). Применяется для обмена с управляющими модулями +по 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 — отправка фреймов | +| 2–17 | 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 MB2–17"] + 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 0x300–0x30F */ 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 +) +``` + +**Зависимости модуля:** | Зависимость | Тип | Описание | | -------------- | ------- | -------------------------------------- | diff --git a/bsp/display/README.md b/bsp/display/README.md index 104c712..13948a7 100644 --- a/bsp/display/README.md +++ b/bsp/display/README.md @@ -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[0–7] | GPIO_B0_04–B0_11 | C8, B8, A8, A9, B9, C9, D9, A10 | +| DATA[8–15] | GPIO_B0_12–B1_03 | | +| DATA[16–23] | GPIO_B1_04–B1_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` | diff --git a/bsp/led/README.md b/bsp/led/README.md index 6bce6e4..811e28e 100644 --- a/bsp/led/README.md +++ b/bsp/led/README.md @@ -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( 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()` | diff --git a/bsp/opto/README.md b/bsp/opto/README.md index 7d03998..1872dff 100644 --- a/bsp/opto/README.md +++ b/bsp/opto/README.md @@ -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 RLY2–4. + +```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()` | diff --git a/bsp/qspi_flash/README.md b/bsp/qspi_flash/README.md index 94f5e30..9877ed5 100644 --- a/bsp/qspi_flash/README.md +++ b/bsp/qspi_flash/README.md @@ -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) | Да | - -Слоты 1–11 обновляются в `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-команды | diff --git a/bsp/sd/README.md b/bsp/sd/README.md index 6cb27b4..d6464d6 100644 --- a/bsp/sd/README.md +++ b/bsp/sd/README.md @@ -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, периферийный режим | -| D0–D3 | GPIO_SD_B0_02–05 | USDHC1_DATA0–3, периферийный режим | -| 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`. diff --git a/bsp/sdram/README.md b/bsp/sdram/README.md index 6ff436d..3056380 100644 --- a/bsp/sdram/README.md +++ b/bsp/sdram/README.md @@ -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-уровневые символы | diff --git a/bsp/tick/README.md b/bsp/tick/README.md index d5676ed..c96fa1c 100644 --- a/bsp/tick/README.md +++ b/bsp/tick/README.md @@ -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` | diff --git a/bsp/uart_host/README.md b/bsp/uart_host/README.md index c9b1afd..5ce90d6 100644 --- a/bsp/uart_host/README.md +++ b/bsp/uart_host/README.md @@ -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`). Пакеты короткие, задержка 1–2 мс приемлема. -- **RX** — ISR пишет в ring buffer, задача/main читает с таймаутом. -- **ISR** — `LPUART1_IRQHandler` определён в модуле, модуль владеет прерыванием целиком. +- **TX** — `LPUART_WriteBlocking`. Пакеты короткие, задержка 1–2 мс приемлема. +- **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` | \ No newline at end of file +| `sdk_lpuart` | PRIVATE | `fsl_lpuart.h`, `fsl_clock.h` | diff --git a/bsp/usb_cdc/README.md b/bsp/usb_cdc/README.md index 3765c94..b07bd32 100644 --- a/bsp/usb_cdc/README.md +++ b/bsp/usb_cdc/README.md @@ -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 ` | `` | Echo-back | -| Команда | Ответ | Описание | -| ------------- | -------- | ---------------- | -| `PING` | `PONG` | Проверка канала | -| `ECHO ` | `` | 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`. diff --git a/docs/DEV_ARCH.md b/docs/DEV_ARCH.md index 6deb33a..767b0db 100644 --- a/docs/DEV_ARCH.md +++ b/docs/DEV_ARCH.md @@ -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["RLY1–4"] + 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 && cd ### 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_` загружает 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 ``` --- diff --git a/docs/HOW_TO_DEBUG.md b/docs/HOW_TO_DEBUG.md index 962960f..1fc921d 100644 --- a/docs/HOW_TO_DEBUG.md +++ b/docs/HOW_TO_DEBUG.md @@ -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 + код ``` -Весь диапазон `0x60000000–0x6000FFFF` умещается в один 64KB-сектор Flash, поэтому стирается и записывается за одну транзакцию — FCB и HAB не перезаписывают друг друга. +Весь диапазон `0x60000000–0x6000FFFF` — один 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 — регистры периферии diff --git a/docs/HOW_TO_FLASH.md b/docs/HOW_TO_FLASH.md index 3d80ef1..fa5c11e 100644 --- a/docs/HOW_TO_FLASH.md +++ b/docs/HOW_TO_FLASH.md @@ -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 и хранится в репозитории — пересоздавать не нужно. +и хранится в репозитории — пересоздавать не нужно. --- diff --git a/docs/testing/PROTOCOL.md b/docs/testing/PROTOCOL.md index bfe7a7b..cefa7c3 100644 --- a/docs/testing/PROTOCOL.md +++ b/docs/testing/PROTOCOL.md @@ -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
таймаут 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"` и предупреждать оператора при +несовпадении ожидаемой версии. diff --git a/docs/testing/hil/HIL_CREATE_TEST.md b/docs/testing/hil/HIL_CREATE_TEST.md index 3c15b4f..8139258 100644 --- a/docs/testing/hil/HIL_CREATE_TEST.md +++ b/docs/testing/hil/HIL_CREATE_TEST.md @@ -2,30 +2,31 @@ ## Обзор стека -```bash -devcontainer хост -───────────────────────────────── ──────────────────────────────────── -tests/target// tools/hil/ - main.c ← C-прошивка с CLI test_.py ← pytest-тесты - CMakeLists.txt conftest.py ← фикстуры (общие) - m5/agent.py ← агент M5 (если нужен) -CMakePresets.json - target-debug-build just/host.just - └── targets: [test_] hil-run, hil- +```mermaid +flowchart LR + subgraph DC["Devcontainer"] + C["tests/target/<name>/\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_<name>.py — pytest\nconftest.py — фикстуры"] + JH["just/host.just\nhil-run, hil-<name>"] + 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() # ← добавить строку +add_subdirectory() # ← добавить ``` --- @@ -153,13 +152,13 @@ add_subdirectory() # ← добавить строку --- -## Шаг 4 — Сборка +## Шаг 4 — Собрать ```bash # В devcontainer: just build::build-hil -# Проверить что новый таргет собрался: +# Проверить: ls build/target-debug/tests/target//test_.elf ``` @@ -167,16 +166,10 @@ ls build/target-debug/tests/target//test_.elf ## Шаг 5 — `conftest.py`: добавить фикстуры -Открыть `tools/hil/conftest.py` и добавить: - -1. Фикстуру загрузки `loaded_` в конец раздела загрузок. -2. Одну строку в `_UART_FIXTURE_MAP` — фабрика `_make_uart_fixture` автоматически - создаст фикстуру `uart_` через контекстный менеджер `_uart_context`. - ### Базовый тест (без M5) ```python -# 1. Фикстура загрузки — добавить в раздел loaded_* +# 1. Фикстура загрузки @pytest.fixture(scope="module") def loaded_(request: pytest.FixtureRequest) -> None: _load_elf( @@ -184,22 +177,19 @@ def loaded_(request: pytest.FixtureRequest) -> None: Path(cfg.BUILD_DIR) / "tests/target//test_.elf", ) -# 2. UART-фикстура — добавить одну строку в словарь +# 2. UART-фикстура — одна строка в словарь _UART_FIXTURE_MAP = { ... - "uart_": "loaded_", # ← добавить + "uart_": "loaded_", } ``` -### Тест с M5 (GPIO, реле, питание) +### Тест с M5 ```python -# 1. Фикстура загрузки — зависимость от m5 гарантирует питание +# 1. Зависимость от m5 гарантирует что питание включено до загрузки ELF @pytest.fixture(scope="module") def loaded_(request: pytest.FixtureRequest, m5: M5Agent) -> None: - """ - Зависит от m5 — питание таргета уже включено к моменту загрузки ELF. - """ _load_elf( request, Path(cfg.BUILD_DIR) / "tests/target//test_.elf", @@ -208,23 +198,18 @@ def loaded_(request: pytest.FixtureRequest, m5: M5Agent) -> None: # 2. UART-фикстура — та же одна строка _UART_FIXTURE_MAP = { ... - "uart_": "loaded_", # ← добавить + "uart_": "loaded_", } ``` **Правило:** если тест управляет железом через M5 — `loaded_` должен явно -зависеть от `m5`. Это гарантирует что питание включено до того как pyOCD -попытается подключиться к MCU. - -> Ручное написание `uart_` фикстур больше не требуется — фабрика -> `_make_uart_fixture` создаёт фикстуру с `_uart_context` (контекстный менеджер, -> гарантирует `ser.close()` при любом исходе). +зависеть от `m5`, иначе pyOCD попытается подключиться до включения питания. --- ## Шаг 6 — `tools/hil/test_.py` -### Базовый тест (без M5) +### Базовый тест ```python """test_.py — HIL тест <что тестируем>.""" @@ -239,23 +224,21 @@ class Test: self.ser = uart_ 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_.py — HIL тест <что тестируем> через M5StampPLC.""" +"""test_.py — HIL тест через M5StampPLC.""" import time import pytest from conftest import uart_cmd -SETTLE_S = 0.15 # ждать после переключения реле +SETTLE_S = 0.15 class Test: @@ -264,24 +247,19 @@ class Test: def _setup(self, uart_, m5): self.ser = uart_ 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_.py — интерактивный HIL-тест <что тестируем>.""" +"""test_.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: 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-` (с флагом `-s`). --- @@ -365,86 +334,50 @@ hil-: --- -## Полный цикл - -```bash -# 1. devcontainer — собрать прошивку -just build::build-hil - -# 2. хост — убедиться что стенд готов (если тест использует M5) -just host::m5-deploy # если менялся agent.py -just host::m5-scan # убедиться что M5 видна - -# 3. хост — запустить только новый тест -just host::hil- - -# 4. хост — загрузить ELF вручную без тестов (для отладки) -uv run --directory tools/hil python load_and_run.py \ - build/target-debug/tests/target//test_.elf - -# 5. хост — запустить один тест -uv run --directory tools/hil pytest test_.py::Test::test_ping -v -``` - ---- - ## Как работают фикстуры -### Цепочка зависимостей +```mermaid +flowchart TB + TF["test_foo()"] + SU["_setup\n(function scope, autouse)"] + UN["uart_<n>\n(module scope)"] + LN["loaded_<n>\n(module scope)"] + M5["m5\n(module scope, если нужен)"] -```bash -test_foo() - └── _setup (function scope, autouse) - ├── uart_ (module scope) ← открыт один раз на весь файл - │ └── loaded_ ← 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_.py - -01_test_uart.py test_.py -───────────────────── ───────────────────── -loaded_host_uart m5 ← создаётся -uart ← создаётся loaded_ - test_ping uart_ ← создаётся - test_echo test_ping -uart.close() test_something - uart_.close() - m5 teardown → power(False) -``` - -Каждый файл — своя загрузка ELF, свой UART-сеанс. MCU перезагружается между файлами. +**Порядок при нескольких файлах:** каждый файл — своя загрузка ELF, свой +UART-сеанс. MCU перезагружается между файлами. --- ## Чеклист -### Автоматический тест (базовый или с M5) +### Автоматический тест ```bash -[ ] tests/target//main.c — C-прошивка с CLI + READY-паттерн -[ ] tests/target//CMakeLists.txt — сборка с bsp_boot_ram +[ ] tests/target//main.c — C-прошивка с CLI + READY-паттерн +[ ] tests/target//CMakeLists.txt — сборка с bsp_boot_ram [ ] tests/target/CMakeLists.txt — add_subdirectory() [ ] CMakePresets.json — добавить test_ в targets [ ] tools/hil/conftest.py — loaded_ + строка в _UART_FIXTURE_MAP -[ ] tools/hil/test_.py — pytest-тесты +[ ] tools/hil/test_.py — pytest-тесты [ ] just/host.just — рецепт hil- (опционально) [ ] just build::build-hil — зелёная сборка -[ ] just host::hil- — зелёный прогон +[ ] just host::hil- — зелёный прогон ``` -### Интерактивный тест (дополнительно к базовому чеклисту) +### Дополнительно для интерактивного теста ```bash [ ] @pytest.mark.interactive — пометить класс в test_.py [ ] just/host.just — рецепт hil- с флагом -s [ ] just host::hil-run-interactive — зелёный прогон -[ ] убедиться что just host::hil-run — NOT в выборке (маркер исключает) +[ ] убедиться что just host::hil-run — НЕ включает этот тест ``` diff --git a/docs/testing/host/HOST_CREATE_TEST.md b/docs/testing/host/HOST_CREATE_TEST.md index 74cf02b..1fc921d 100644 --- a/docs/testing/host/HOST_CREATE_TEST.md +++ b/docs/testing/host/HOST_CREATE_TEST.md @@ -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//test_.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_] 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//test_.c */ -#include "unity.h" -#include ".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//test_.c */ -#include "unity.h" -#include "fff.h" - -DEFINE_FFF_GLOBALS; /* 1. ровно один раз на весь .c файл */ - -#include "fsl_.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/.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_.h */ -#pragma once -#include - -/* Минимально необходимые типы */ -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_ - SOURCES /test_.c - ${PROJECT_SOURCE_DIR}//.c - INCLUDES - ${PROJECT_SOURCE_DIR}//include -) - -# Категория B — с mocks -add_host_test( - NAME test_ - SOURCES /test_.c - ${PROJECT_SOURCE_DIR}/bsp//src/.c - INCLUDES - ${PROJECT_SOURCE_DIR}/bsp//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_" - ] -} -``` - -То же самое для `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_ -V - -# Напрямую — виден полный вывод Unity без CTest-обёртки -./build/host-debug/tests/host/test_ +# 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-модули должны явно включать ``, ``, `` — не полагаться на транзитивное подтягивание через NXP SDK. На хосте этот транзит отсутствует, компиляция упадёт с `undeclared identifier 'size_t'`. - ---- - -## Полный цикл +### Режим Б — прошить через SWD, затем отладить ```bash -# 1. Создать тестовый файл -tests/host//test_.c +# 1. DevContainer +just build::hab-firmware-test-debug -# 2. Создать stub-хедер (если категория B и stub не существует) -tests/host/mocks/fsl_.h +# 2. Хост +just host::flash-swd-test-debug -# 3. Добавить вызов add_host_test() в -tests/host/CMakeLists.txt +# 3. ⚡ Power cycle платы (обязательно) -# 4. Добавить "test_" в 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//test_.c — тест с main(), setUp(), tearDown() -[ ] tests/host/mocks/fsl_.h — stub (только категория B, если нет) -[ ] tests/host/CMakeLists.txt — add_host_test(NAME test_ ...) -[ ] CMakePresets.json — добавить test_ в host-debug-build -[ ] just build::test-host — зелёный прогон +0x60000000 w25q128_fdcb.bin (512 байт) — FCB +0x60000200 0xFF × 3584 байт — padding +0x60001000 firmware_test_hab.bin — IVT + DCD + код +``` + +Весь диапазон `0x60000000–0x6000FFFF` — один 64KB сектор: стирается и +записывается за одну транзакцию. + +--- + +## RTT-логи + +SEGGER RTT включён только в Debug-сборках (`SEGGER_RTT_ENABLED=ON`). +После старта отладки вкладка `TERMINAL → RTT` принимает вывод канала 0. +`cortex-debug` находит адрес буфера по символу `_SEGGER_RTT` из ELF. + +```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 ``` diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index 9c666f0..e6e98db 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -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 diff --git a/firmware/test/PLAN.md b/firmware/test/PLAN.md index 050d144..a6304b2 100644 --- a/firmware/test/PLAN.md +++ b/firmware/test/PLAN.md @@ -1,6 +1,6 @@ -# firmware_test — Plan of Development +# firmware_test — План разработки -> Версия: 0.4 | Обновлён после завершения Этапов 1–3 (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 = " 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? diff --git a/firmware/test/src/test_runner.c b/firmware/test/src/test_runner.c index f352d82..130ed0f 100644 --- a/firmware/test/src/test_runner.c +++ b/firmware/test/src/test_runner.c @@ -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])) diff --git a/firmware/test/src/tests/README.md b/firmware/test/src/tests/README.md index 3fdd261..d1842ca 100644 --- a/firmware/test/src/tests/README.md +++ b/firmware/test/src/tests/README.md @@ -1,730 +1,442 @@ -# firmware_test — Тест-модули +# firmware_test — Руководство по тестированию -> Расположение: `firmware/test/src/tests/README.md` -> -> Каждый тест-модуль реализует интерфейс `test_module_t` и регистрируется -> в реестре `test_runner.c`. Этот документ описывает что именно проверяет -> каждый тест, какие гарантии даёт, какие аппаратные инварианты должны -> соблюдаться, и каковы ограничения. +Тестовая прошивка входного контроля платы MIMXRT1052CVJ5B. +Транспорт: USB CDC ACM (J2). Протокол: JSON-lines v2, один JSON-объект на строку. +Загружается в RAM через BootROM USB SDP — без предварительной прошивки загрузчика. + +> Версия прошивки: `0.1.4` (`FIRMWARE_TEST_VERSION` в `protocol.h`) --- -## 1 Содержание +## Протокол — справочник сообщений -- [Как читать эту таблицу](#как-читать-эту-таблицу) -- [test_sdram — SDRAM 32 MB](#test_sdram--sdram-32-mb) -- [test_qspi — QSPI Flash W25Qxx](#test_qspi--qspi-flash-w25qxx) -- [test_usd — uSD SDIO](#test_usd--usd-sdio) -- [test_display — Display RGB888](#test_display--display-rgb888) -- [test_buttons — Test_But_1 / Test_But_2](#test_buttons--test_but_1--test_but_2) *(запланирован)* -- [test_can — CAN loopback](#test_can--can-loopback) *(запланирован)* -- [test_uart_ttl — UART TTL](#test_uart_ttl--uart-ttl) *(запланирован)* -- [test_uart_iso — UART ISO / RS_RX](#test_uart_iso--uart-iso--rs_rx) *(запланирован)* -- [test_opto — Opto-in EXT_IN1/IN2](#test_opto--opto-in-ext_in1in2) *(запланирован)* +### Входящие сообщения (host → target) + +| Тип | Пример | Описание | +| --------- | ------------------------------------------------ | --------------------------- | +| `cmd` | `{"type":"cmd","cmd":"ping"}` | Проверка канала | +| `cmd` | `{"type":"cmd","cmd":"run","id":"sdram"}` | Запустить один тест по ID | +| `cmd` | `{"type":"cmd","cmd":"run_all"}` | Запустить все тесты реестра | +| `confirm` | `{"type":"confirm","id":"usd","confirmed":true}` | Ответ оператора на запрос | + +### Исходящие события (target → host) + +| Тип | Ключевые поля | Описание | +| ---------------------------- | ---------------------------------------- | ------------------------ | +| `session_start` | `fw`, `target`, `uptime_ms` | Прошивка готова к работе | +| `test_begin` | `id`, `name`, `critical` | Тест запущен | +| `test_result` | `id`, `status`, `ms`, `detail` | Результат теста | +| `confirm_request` | `id`, `prompt`, `timeout_ms` | Запрос оператору | +| `progress` | `test`, `step`, `status` | Прогресс внутри теста | +| `summary` | `passed`, `failed`, `skipped`, `overall` | Итог `run_all` | +| `pong` | — | Ответ на `ping` | +| `{"ok":false,"error":"..."}` | `error` | Ошибка протокола | + +**Возможные статусы `test_result`:** `pass` / `fail` / `skip` + +**Коды ошибок в `error`:** + +| Код | Причина | +| -------------- | --------------------------------------------------------------- | +| `BUSY` | Предыдущий тест ещё выполняется | +| `UNKNOWN_TEST` | ID теста не найден в реестре | +| `PARSE_ERR` | Не удалось разобрать JSON (нет поля `type`, `cmd`, `id` и т.д.) | +| `UNKNOWN_CMD` | Неизвестный тип сообщения или команда | --- -## 2 Как читать эту таблицу +## Подключение и начало сессии -**Critical** — при провале в `run_all` все последующие тесты получают `SKIP`. -Некритические тесты могут упасть без остановки прогона. - -**HIL** — тест требует внешних сигналов от M5StampPLC. -Без стенда тест вернёт `SKIP` или `FAIL`. - -**Тип confirm:** - -- `pre_confirm` — test_runner ждёт JSON-ответа оператора до запуска `run()`. -- `в run()` — тест сам вызывает `test_runner_wait_confirm()` внутри. -- `prompt only` — отправляет `confirm_request` как UI-подсказку, - ответ не ожидается (тест детектирует физическое событие сам). -- `—` — неинтерактивный тест. - ---- - -## 3 test_sdram — SDRAM 32 MB - -**Файл:** `test_sdram.c` -**ID:** `sdram` -**Critical:** ✅ | **HIL:** ❌ | **Confirm:** — - -### 3.1 Аппаратный контекст - -| Параметр | Значение | -| --------------- | ------------------------------------------------- | -| Чип | MT48LC16M16A2 | -| Объём | 32 MB | -| Шина данных | 16 бит | -| Интерфейс MCU | SEMC (0x402F0000) | -| Базовый адрес | 0x80000000 | -| Тестовый регион | 0x80200000 — 0x80A00000 | -| MPU | Region 8: Normal WB cacheable (BOARD_MPU_SDRAM=1) | - -### 3.2 Инварианты выполнения - -- SEMC инициализирован DCD **до** `main()` — `bsp_sdram_init()` только верифицирует. -- MPU Region 8 (`Normal WB`) активен — кэш включён для SDRAM. -- MPU Region 9 (`Non-cacheable`, 0x81E00000, 2MB) активен — USB DMA изолирован. -- Тестовый регион не пересекается с `.data`/`.bss` прошивки (смещение +2 MB от базы). -- Тестовый регион не пересекается с non-cacheable регионом (граница 0x81E00000). -- Сброс кэша (`SCB_CleanDCache_by_Addr`) выполняется **после каждого** write-прохода — - readback всегда из физической SDRAM, не из кэша. - -### 3.3 Что тестирует — четыре фазы - -#### 3.3.1 Фаза 1: Address bus (~1 мс) - -Записывает уникальный байт в 24 позиции на степенях двойки -(2^0..2^23 от TEST_BASE), каждую с индивидуальным cache line flush. - -**Покрытие:** все 24 адресных бита MT48LC16M16A2 (13 row + 9 col + 2 bank). - -**Ловит:** - -- Address aliasing — замыкание адресных линий SEMC. -- Неправильное подключение адресных линий к чипу. - -**Не ловит:** - -- Деградацию отдельных ячеек вне точек степеней двойки. - ---- - -#### 3.3.2 Фаза 2: Data bus (~1 с) - -Walking ones (0x01, 0x02, ..., 0x80, 0x01, ...) и его инверсия -на регионе 64 KB. - -**Покрытие:** все 8 бит шины данных. - -**Ловит:** - -- Stuck-at-0 и stuck-at-1 фолты на битах шины данных. -- Обрыв линии DATA между MCU и чипом. - -**Не ловит:** - -- Coupling между несмежными битами (для этого — фаза 3). - ---- - -#### 3.3.3 Фаза 3: Sequential integrity (~25 с) - -Address pattern (`offset & 0xFF`) и его инверсия на регионе 2 MB, -два паттерна × два прохода (write → flush → verify). - -**Покрытие:** 2 MB непрерывного адресного пространства. - -**Ловит:** - -- Coupling faults между соседними ячейками. -- Деградированные ячейки в тестируемом регионе. -- Частичный address aliasing внутри 2 MB. - -**Не ловит:** - -- Деградацию ячеек вне тестируемых 2 MB (всего 32 MB в чипе). - ---- - -#### 3.3.4Фаза 4: Retention (~2 с) - -Address pattern на 256 KB: запись → `flush_dcache` → ожидание 200 мс → верификация. - -200 мс ≈ 3 полных refresh-периода MT48LC16M16A2 (период = 64 мс). - -**Покрытие:** 256 KB с задержкой на несколько refresh-циклов. - -**Ловит:** - -- Refresh timing failures — ячейки теряют данные между refresh-циклами. -- Деградацию конденсаторов ячеек памяти (capacitor leakage). - -**Не ловит:** - -- Retention failures при температурных крайностях. - ---- - -### 3.4Гарантии теста при PASS - -- Все 24 адресных бита работают независимо без aliasing. -- Все 8 бит шины данных переключаются корректно. -- 2 MB последовательных ячеек не имеют coupling faults. -- 256 KB удерживают данные минимум через 3 refresh-цикла. -- SEMC контроллер инициализирован и отвечает. - -### 3.5 Интерпретация FAIL - -`detail` содержит: `addr=0xXXXXXXXX exp=0xXX got=0xXX` - -| Диапазон addr | Вероятная фаза | Диагноз | -| -------------------------------------------- | -------------- | ------------------------------ | -| `0x80200000` — `0x80A00000` (степени двойки) | Фаза 1 | Address aliasing | -| `0x80200000` — `0x80210000` | Фаза 2 | Stuck-at на шине данных | -| `0x80200000` — `0x80400000` | Фаза 3 | Coupling или деградация ячейки | -| `0x80200000` — `0x80240000` | Фаза 4 | Refresh timing failure | - -**Анализ `exp` XOR `got`:** биты где `(exp ^ got) != 0` — сбойные линии шины данных. - -### 3.6 Ограничения - -- Не покрывает все 32 MB (только 2 MB для sequential). -- Не проверяет retention при нагреве или низком напряжении питания. -- Не является заменой полного March C− алгоритма. -- Во время теста (~28 с) USB CDC занят, `ping` не отвечает. - -### 3.7 Типичное время выполнения - -| Фаза | ~Время | -| ----------- | --------- | -| Address bus | < 1 мс | -| Data bus | ~1 с | -| Sequential | ~25 с | -| Retention | ~2 с | -| **Итого** | **~28 с** | - ---- - -## 4 test_qspi — QSPI Flash W25Qxx - -**Файл:** `test_qspi.c` -**ID:** `qspi` -**Critical:** ✅ | **HIL:** ❌ | **Confirm:** — - -### 4.1 Аппаратный контекст - -| Параметр | Значение | -| --------------- | ------------------------------------------- | -| Чипы | W25Q64 / W25Q128 / W25Q256 / W25Q512 | -| Объём | 8 / 16 / 32 / 64 MB | -| Интерфейс MCU | FlexSPI1, порт A1 | -| BSP | `bsp_qspi` | -| Тестовый сектор | `flash_size - 4KB` (последний, динамически) | - -### 4.2Инварианты выполнения - -- `bsp_qspi_init()` вызывается в `init()` — если чип не опознан, `run()` возвращает FAIL. -- Слот 0 FlexSPI LUT (XIP read, задан FDCB) **никогда не изменяется** — прошивка - выполняется XIP на протяжении всего теста. -- Во время операций stирания/записи (~45–150 мс) Flash находится в состоянии BUSY. - W25Q64/128/256/512 допускают READ-команды в состоянии BUSY — XIP-фетчи инструкций - продолжают работать. -- Тестовый сектор расположен в конце Flash и **не пересекается** с прошивкой - (прошивка занимает первые несколько MB). -- `bsp_usb_cdc_poll()` вызывается вокруг каждой блокирующей операции — USB CDC - остаётся отзывчивым во время теста. - -### 4.3 Что тестирует — четыре шага - -#### 4.3.1 Шаг 1: JEDEC ID (~1 мс) - -Команда 0x9F. Читает manufacturer ID и device ID. - -**Проверяет:** - -- `manufacturer_id == 0xEF` (Winbond). -- `capacity_byte` принадлежит одному из: 0x17 (64Mbit), 0x18 (128Mbit), - 0x19 (256Mbit), 0x20 (512Mbit). - -По результату вычисляется адрес тестового сектора: -`test_addr = bsp_qspi_flash_size() - BSP_QSPI_SECTOR_SIZE`. - -**Ловит:** - -- FlexSPI контроллер не инициализирован или не отвечает. -- Неизвестный / неподдерживаемый чип. -- Обрыв или неправильное подключение QSPI-шины. - -**Не ловит:** - -- Деградацию конкретных ячеек памяти. - ---- - -#### 4.3.2 Шаг 2: Erase + Verify (~500 мс) - -Стирает тестовый сектор (4KB). Читает первые 256 байт сектора. -Все байты должны быть `0xFF`. - -**Покрытие:** способность чипа выполнить Sector Erase и корректно сообщить о завершении. - -**Ловит:** - -- Erase command не принимается чипом (неправильный opcode / режим адресации). -- SR1.WIP не снимается после стирания (erase не завершился). -- Данные после стирания не `0xFF` (ячейки застряли в `0x00`). - -**Не ловит:** - -- Проблемы с ячейками вне первой страницы тестового сектора. - ---- - -#### 4.3.3 Шаг 3: Write + Read + Compare (~200 мс) - -Паттерн `byte[i] = i & 0xFF` (256 байт). Записывает первую страницу -тестового сектора командой Quad Page Program. Читает обратно командой -IP Quad Output Read (слот 11). Сравнивает побайтово. - -**Покрытие:** полный цикл write → read на 256 байтах. - -**Ловит:** - -- Ошибки Page Program (неправильный opcode / адрес / данные). -- Ошибки IP Read (неправильный opcode / адрес). -- Stuck-at bits в ячейках тестовой страницы. -- Шина данных Flash (все 4 линии Quad-режима). - -**Не ловит:** - -- Деградацию ячеек вне первой страницы тестового сектора. -- Aliasing адресов (это проверяет Шаг 4). - ---- - -#### 4.3.4 Шаг 4: Address range — только W25Q256/512 (~100 мс) - -Для чипов с `flash_size > 16 MB`. -Для W25Q64/128 шаг пропускается (24-bit покрывает весь чип). - -**Проблема, которую ловит:** - -При 24-bit адресации адрес обрезается до 24 бит. Для W25Q256/512 -last-sector находится выше 16 MB: +При старте прошивка ждёт CDC-подключение хоста (LED_HEARTBEAT мигает). +После подключения немедленно отправляет `session_start`: ```bash -W25Q256: last sector = 0x1FFF000, 0x1FFF000 & 0xFFFFFF = 0xFFF000 -W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000 +← {"type":"session_start","fw":"0.1.4","target":"IMXRT1052","uptime_ms":1108} ``` -Если dedicated 4-byte opcodes не работают (чип игнорирует старший байт -адреса), Шаг 3 **формально проходит** — запись и чтение ошибаются в одно и -то же место (`0xFFF000`) одинаково. Алиасирование остаётся незамеченным. - -**Метод:** - -После Шага 3 `test_addr` содержит паттерн `i & 0xFF`. Записываем `0xAA` -в anchor-сектор (`0x00FFF000`). Читаем `test_addr`: - -- Получаем `i & 0xFF` → `test_addr` — это физически другой сектор → `PASS`. -- Получаем `0xAA` → `test_addr` физически совпадает с anchor → алиасирование - подтверждено → dedicated 4-byte opcodes не работают → `FAIL`. - -**Ловит:** - -- 24-bit алиасирование адресов для W25Q256/512. -- Несовместимость LUT dedicated 4-byte opcodes с конкретным чипом. - -**Не ловит:** - -- Алиасирование на W25Q64/128 (невозможно по архитектуре). - ---- - -### 4.4 Гарантии теста при PASS - -- FlexSPI1 инициализирован и отвечает. -- Подключён поддерживаемый Winbond Flash с ожидаемым JEDEC ID. -- Команды Sector Erase, Quad Page Program, IP Read работают корректно. -- Для W25Q256/512: dedicated 4-byte address opcodes функционируют и - адресуют физически разные секторы выше и ниже 16 MB. - -### 4.5 Интерпретация FAIL - -`detail` содержит текстовое описание шага и причины: - -| Паттерн `detail` | Шаг | Вероятный диагноз | -| -------------------------------- | --- | --------------------------------------------- | -| `JEDEC: read failed` | 1 | FlexSPI не инициализирован / нет связи | -| `JEDEC: mfr=0xXX exp=0xEF` | 1 | Неизвестный производитель чипа | -| `JEDEC: unknown cap=0xXX` | 1 | Незнакомая ёмкость, чип не в списке поддержки | -| `erase: sector erase failed` | 2 | FlexSPI ошибка при erase command | -| `erase verify failed at 0x…` | 2 | Сектор не стёрся, ячейки застряли | -| `rw: page write failed` | 3 | FlexSPI ошибка при Page Program | -| `rw mismatch at 0x… exp=… got=…` | 3 | Stuck-at bit или ошибка шины данных | -| `addr alias: 0x… mirrors 0x…` | 4 | Алиасирование — 4-byte opcodes не работают | -| `addr range mismatch at 0x…` | 4 | Неожиданные данные при чтении last sector | - -**Анализ `rw mismatch`:** `exp XOR got` — биты, где `(exp ^ got) != 0`, -соответствуют сбойным линиям Quad-шины. - -### 4.6 Ограничения - -- Тестирует только **1 страницу** (256 байт) — не покрывает весь объём чипа. -- Не проверяет endurance (многократные write/erase циклы). -- Не является заменой полного тестирования Flash (march-алгоритмы). -- Шаг 4 не проверяет адреса в диапазоне 16–32 MB (только крайние точки). - -### 4.7 Типичное время выполнения - -| Шаг | Чип | ~Время | -| ----------------- | ----------- | ----------- | -| 1: JEDEC ID | все | < 1 мс | -| 2: Erase + Verify | все | ~500 мс | -| 3: Write + Read | все | ~200 мс | -| 4: Address range | W25Q64/128 | 0 мс (skip) | -| 4: Address range | W25Q256/512 | ~100 мс | -| **Итого** | W25Q64/128 | **~700 мс** | -| **Итого** | W25Q256/512 | **~800 мс** | - ---- - -## 5 test_usd — uSD SDIO - -**Файл:** `test_usd.c` -**ID:** `usd` -**Critical:** ❌ | **HIL:** ❌ | **Confirm:** pre_confirm - -### 5.1 Аппаратный контекст - -| Параметр | Значение | -| ----------------- | --------------------------------------------------------------- | -| Интерфейс MCU | USDHC1 | -| CLK / CMD / D0–D3 | GPIO_SD_B0_00–05 | -| Card Detect | GPIO_B1_12 → USDHC1_CD_B → `USDHC_GetPresentStatusFlags` | -| Питание карты | GPIO_AD_B1_03 (SdPwr, active-low, управляется SDK) | -| Drive FatFS | `2:/` | -| BSP | `bsp_sd` (host init/deinit/card detect) + `firmware_test_fatfs` | - -### 5.2 Инварианты выполнения - -- `bsp_sd_init()` вызывается в `init()`. При провале `run()` возвращает FAIL немедленно. -- Все ресурсы (файл, mount, host) освобождаются в `deinit()` — вызывается test_runner - всегда, включая FAIL и SKIP. -- Тестовый файл `2:/FWTEST.TMP` удаляется при любом исходе. -- `bsp_usb_cdc_poll()` вызывается вокруг каждой блокирующей операции. - -### 5.3 Что тестирует — четыре шага - -| Шаг | Действие | Progress event | ~Время | -| ------------ | ------------------------------------------ | ------------------- | -------- | -| card_detect | `bsp_sd_is_inserted()` | `step:card_detect` | < 1 мс | -| mount | `f_mount(&fs, "2:/", 1)` | `step:mount` | < 200 мс | -| write | `f_open` + `f_write` 4 KB + `f_close` | `step:write` | < 500 мс | -| read_compare | `f_open` + `f_read` + `memcmp` + `f_close` | `step:read_compare` | < 200 мс | - -**Паттерн:** `byte[i] = i & 0xFF`, 4096 байт. Детектирует stuck-at-0/1 и partial write. - -### 5.4 Гарантии теста при PASS - -- USDHC host инициализирован и карта подключена. -- FatFS корректно монтирует раздел (FAT32 / exFAT). -- Запись и чтение 4 KB совпадают побайтово. -- Тестовый файл удалён с карты. - -### 5.5 Интерпретация FAIL - -| Паттерн `detail` | Причина | -| ---------------------------------------------- | ----------------------------------------------------- | -| `no card detected` | Карта не вставлена или CD не работает | -| `sd init failed` | USDHC host не инициализировался | -| `mount failed: N` | FatFS не может прочитать файловую систему (FR code N) | -| `open failed: N` | Нет места или файловая система только для чтения | -| `write failed: N` / `write incomplete: X/4096` | Ошибка записи на карту | -| `read failed: N` | Ошибка чтения | -| `compare failed at offset N` | Данные не совпадают — битый сектор карты | - -### 5.6 Поведение без карты / отказ оператора - -- Оператор не ответил за 30 с → `TEST_STATUS_SKIP`, `detail: "confirm timeout"`. -- Оператор нажал "отказ" → `TEST_STATUS_SKIP`, `detail: "operator declined"`. -- Тест не critical → `run_all` продолжает выполнение остальных тестов. - -### 5.7 Типичное время выполнения - -| Этап | ~Время | -| ------------------- | ----------- | -| SD init + card init | ~200 мс | -| Mount | ~100 мс | -| Write 4 KB | ~400 мс | -| Read + Compare 4 KB | ~150 мс | -| **Итого** | **~850 мс** | - ---- - -## 6 test_display — TFT-дисплей RGB888 - -**Файл:** `test_display.c` -**ID:** `display` -**Critical:** ❌ | **HIL:** ❌ | **Confirm:** в run() - -### 6.1 Аппаратный контекст - -| Параметр | Значение | -| -------------- | --------------------------------------------------- | -| Контроллер | ELCDIF (RGB888, DE mode) | -| Дисплейный IC | HX8264-D02 (TFT7/TFT8/TFT10) | -| Фреймбуфер | NonCacheable SDRAM, `AT_NONCACHEABLE_SECTION_ALIGN` | -| Подсветка | GPIO1[20] (LcdLed), active-high | -| Горизонт. скан | GPIO1[28] (LcdLR / SHLR контроллера) | -| Вертикал. скан | GPIO1[30] (LcdUd / UPDN контроллера) | -| MODE | GPIO1[29] — HIGH обязательно (DE mode для ELCDIF) | -| DITHB | GPIO1[31] — HIGH (dithering disable, IC default) | -| Тип дисплея | CMake-define `DISPLAY_TEST_TYPE` (сейчас TFT8) | - -### 6.2 Инварианты выполнения - -- `bsp_display_init()` вызывается в `init()`. При провале `run()` возвращает FAIL немедленно. -- Фреймбуфер `g_s_framebuf` статический, в NonCacheable SDRAM — ELCDIF DMA - всегда читает актуальные данные без cache flush. -- `bsp_usb_cdc_poll()` вызывается в циклах заливки буфера и ожидания FRAME_DONE — - USB CDC остаётся отзывчивым во время теста. -- После теста `bsp_display_deinit()` восстанавливает ROTATE_0 и выключает подсветку. - -### 6.3 Что тестирует — два этапа - -#### 6.3.1 Этап 1: Цвет (все типы дисплеев) - -Четыре шага: Red → Green → Blue → White. - -На каждом шаге прошивка заливает фреймбуфер сплошным цветом XRGB8888, -ждёт ISR FRAME_DONE, затем ждёт визуального подтверждения оператора -(таймаут 15 с). - -**Ловит:** - -- Неработающую подсветку. -- Обрыв или неправильное подключение отдельных RGB-каналов шины данных. -- Полное отсутствие изображения (ELCDIF / питание дисплея). - -**Не ловит:** - -- Деградацию отдельных пикселей (stuck pixel). -- Проблемы с яркостью и гамма-коррекцией. - ---- - -#### 6.3.2 Этап 2: Проверка LR/UD пинов (TFT7/TFT8/TFT10) - -Диагностирует непропаянные ножки `LcdLR` (GPIO1[28]) и `LcdUd` (GPIO1[30]). - -**Паттерн:** левая половина экрана RED, правая BLUE. - -**Шаг 1 — базовая ориентация (`ROTATE_0`):** -прошивка устанавливает `LR=1 UD=0`, заливает паттерн, оператор -подтверждает что левая зона красная, правая синяя. - -**Шаг 2 — горизонтальный флип (`FLIP_HORIZONTAL`):** -прошивка устанавливает `LR=0 UD=0` — контроллер HX8264-D02 меняет -направление источника (SHLR). При исправном LR-пине цветовые зоны -меняются местами. Оператор подтверждает изменение. - -После теста прошивка восстанавливает `ROTATE_0` независимо от результата. - -> **Примечание по контроллеру HX8264-D02:** -> LR (SHLR) управляет **горизонтальным** направлением источников — -> меняет местами левую/правую части. UD (UPDN) управляет **вертикальным** -> направлением затвора. Для детектирования непропаянного LR-пина -> используется горизонтальный паттерн и `FLIP_HORIZONTAL`. - -**Ловит:** - -- Непропаянный `LcdLR` (GPIO1[28]) — горизонтальные зоны не меняются. -- Непропаянный `LcdUd` (GPIO1[30]) — тест не проверяет UD напрямую, - но бракованная плата с обоими непропаянными пинами также не пройдёт - шаг 1 (непредсказуемая ориентация при старте). - -**Не ловит:** - -- Изолированный непропай `LcdUd` при исправном `LcdLR`. - ---- - -### 6.4 Гарантии теста при PASS - -- ELCDIF инициализирован, фреймбуфер DMA работает корректно. -- Все три RGB-канала шины данных функционируют. -- Подсветка включается и отключается. -- Пин `LcdLR` (GPIO1[28]) физически пропаян и реагирует на GPIO-запись. - -### 6.5 Интерпретация FAIL - -| `detail` | Вероятный диагноз | -| -------------------------------- | -------------------------------------------- | -| `display init failed` | Неверный `DISPLAY_TEST_TYPE` или нет питания | -| `display_red not confirmed` | Нет изображения / канал R / подсветка | -| `display_green not confirmed` | Канал G мёртв | -| `display_blue not confirmed` | Канал B мёртв | -| `display_white not confirmed` | Подсветка или несколько RGB-каналов | -| `display_rot_base not confirmed` | Нет изображения в паттерне (ELCDIF) | -| `display_rot_lr not confirmed` | **LcdLR (GPIO1[28]) не пропаян** | - -### 6.6 Последовательность событий протокола +### Проверка канала (ping) ```bash -test_begin -→ confirm_request(display_red) ← оператор видит сплошной красный -→ confirm_request(display_green) ← сплошной зелёный -→ confirm_request(display_blue) ← сплошной синий -→ confirm_request(display_white) ← сплошной белый -→ confirm_request(display_rot_base) ← левая RED / правая BLUE (ROTATE_0) -→ confirm_request(display_rot_lr) ← цветовые зоны поменялись (FLIP_HORIZONTAL) -test_result +→ {"type":"cmd","cmd":"ping"} +← {"type":"pong"} ``` -> **Важно:** `test_display` не использует `pre_confirm_prompt`. -> Первый confirm_request (`display_red`) отправляется изнутри `run()` -> после `test_begin`. Хост должен отвечать только на события типа -> `confirm_request`, не опережая их. - -### 6.7 Зависимости - -| Зависимость | Описание | -| ------------- | ------------------------------------------ | -| `bsp_display` | ELCDIF init/deinit, rotation, frame buffer | -| `bsp_usb_cdc` | `bsp_usb_cdc_poll()` в циклах ожидания | - -### 6.8 Типичное время выполнения - -| Этап | ~Время | -| -------------------- | -------------- | -| init (ELCDIF + GPIO) | < 10 мс | -| 4 × цветовой шаг | 4 × 15 с (max) | -| 2 × шаг ротации | 2 × 15 с (max) | -| **Итого (max)** | **~90 с** | -| **Итого (типичный)** | **~40–60 с** | - -## 7 test_buttons — Test_But_1 / Test_But_2 - -**Файл:** `test_buttons.c` *(не реализован — Этап 5)* -**ID:** `buttons` -**Critical:** ❌ | **HIL:** ❌ | **Confirm:** prompt only - -### 7.1 Аппаратный контекст - -| Кнопка | Пин MCU | GPIO | -| ---------- | ---------- | --------- | -| Test_But_1 | GPIO_B1_14 | GPIO2[30] | -| Test_But_2 | GPIO_B1_15 | GPIO2[31] | - -### Что будет тестировать - -Два шага. Таргет отправляет `confirm_request` как инструкцию оператору -и детектирует нажатие через `bsp_button` — JSON confirm не нужен. - -| Шаг | ID | Промпт | Таймаут | -| -------- | ------------ | --------------------------- | ------- | -| Кнопка 1 | `btn1_press` | "Нажмите кнопку Test_But_1" | 10 с | -| Кнопка 2 | `btn2_press` | "Нажмите кнопку Test_But_2" | 10 с | - -### Гарантии при PASS - -- Оба GPIO входа корректно регистрируют нажатие. -- `bsp_button` debounce логика работает. - --- -## test_can — CAN loopback +## SDRAM — контроль оперативной памяти -**Файл:** `test_can.c` *(не реализован — Этап 6)* -**ID:** `can` -**Critical:** ❌ | **HIL:** ✅ | **Confirm:** — +| Параметр | Значение | +| ---------------- | --------------------------------------------- | +| ID | `sdram` | +| Критичный | ✅ Да — при FAIL остальные тесты получают SKIP | +| HIL | ❌ Нет | +| Тип | Self-test | +| Время выполнения | ~15–30 с | -### Аппаратный контекст +Четыре фазы: -| Параметр | Значение | -| -------- | ----------------------------------- | -| BSP | `bsp_can` ✅ | -| M5 | CAN интерфейсная плата → шина платы | +1. **Address bus** — проверка 24 адресных бит (2⁰…2²³ от `TEST_BASE`) +2. **Data bus** — walking ones + инверсия, 64 KB +3. **Sequential integrity** — address pattern + инверсия, 2 MB +4. **Retention** — 256 KB: запись → flush → 200 мс → верификация -### Что будет тестировать - -M5StampPLC отправляет CAN фрейм → плата принимает → сравниваем ID и данные. - -### Гарантии при PASS - -- CAN контроллер и трансивер работают. -- Принятый фрейм совпадает с отправленным по ID и payload. - ---- - -## test_uart_ttl — UART TTL - -**Файл:** `test_uart_ttl.c` *(не реализован — Этап 6)* -**ID:** `uart_ttl` -**Critical:** ❌ | **HIL:** ✅ | **Confirm:** — - -### Аппаратный контекст - -| Параметр | Значение | -| -------- | --------------------- | -| BSP | `bsp_uart_host` ✅ | -| M5 | UART ↔ UART TTL платы | - -### Что будет тестировать - -M5 отправляет пакет → плата получает → echo обратно → M5 верифицирует. - ---- - -## test_uart_iso — UART ISO / RS_RX - -**Файл:** `test_uart_iso.c` *(не реализован — Этап 6)* -**ID:** `uart_iso` -**Critical:** ❌ | **HIL:** ✅ | **Confirm:** — - -### Аппаратный контекст - -| Параметр | Значение | -| -------- | --------------------------------------- | -| BSP | `bsp_opto` (rs_as_gpio=true) ✅ | -| Пин MCU | GPIO_AD_B1_07 / GPIO1[23] | -| Оптопара | PS2801-4 (неинвертирующая, active-HIGH) | -| M5 | RLY2 → RS_RX | - -### Что будет тестировать - -M5 RLY2 активирует оптовход RS_RX → плата детектирует через `bsp_opto`. -M5 RLY2 деактивирует → плата детектирует inactive. - -### Инварианты - -- `bsp_opto` инициализирован с `rs_as_gpio=true`. -- Пин GPIO_AD_B1_07 переведён в GPIO INPUT (не LPUART3_RX). - ---- - -## test_opto — Opto-in EXT_IN1/IN2 - -**Файл:** `test_opto.c` *(не реализован — Этап 6)* -**ID:** `opto` -**Critical:** ❌ | **HIL:** ✅ | **Confirm:** — - -### Аппаратный контекст - -| Канал | Пин MCU | GPIO | Оптопара | M5 | -| ------- | ------------- | --------- | -------- | ---- | -| EXT_IN1 | GPIO_AD_B1_06 | GPIO1[22] | PS2801-4 | RLY3 | -| EXT_IN2 | GPIO_AD_B1_05 | GPIO1[21] | PS2801-4 | RLY4 | - -### Что будет тестировать - -Каждый канал независимо: M5 активирует реле → плата детектирует ACTIVE → -M5 деактивирует → плата детектирует INACTIVE. - -### Гарантии при PASS - -- Оба оптоизолированных входа корректно детектируют HIGH/LOW. -- Debounce логика `bsp_opto` (MODE_LEVEL) работает корректно. - ---- - -## Порядок в реестре `test_runner.c` - -```c -static const test_module_t *const k_registry[] = { - &K_TEST_SDRAM, /* critical — первым */ - &K_TEST_QSPI, /* critical */ - &K_TEST_USD, /* non-critical, interactive, pre_confirm */ - &K_TEST_DISPLAY, /* non-critical, interactive, confirm в run() */ - &K_TEST_BUTTONS, /* non-critical, interactive, prompt only */ - &K_TEST_CAN, /* non-critical, HIL */ - &K_TEST_UART_TTL, /* non-critical, HIL */ - &K_TEST_UART_ISO, /* non-critical, HIL */ - &K_TEST_OPTO, /* non-critical, HIL */ -}; +```mermaid +sequenceDiagram + participant H as HOST + participant T as TARGET + H->>T: run("sdram") + T->>H: test_begin + Note over T: ~15–30 с: 4 фазы + T->>H: test_result: pass/fail ``` -Critical тесты идут первыми — при их провале HIL и интерактивные тесты -пропускаются автоматически, экономя время диагностики. +**PASS:** + +```bash +→ {"type":"cmd","cmd":"run","id":"sdram"} +← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} +← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} +``` + +**FAIL — пример адресной ошибки:** + +```bash +← {"type":"test_result","id":"sdram","status":"fail","ms":1203, + "detail":"addr=0x80200001 exp=0x02 got=0xFF"} +``` + +**FAIL — SEMC не инициализирован (DCD не отработал):** + +```bash +← {"type":"test_result","id":"sdram","status":"fail","ms":0, + "detail":"SEMC not ready — DCD failed?"} +``` + +--- + +## QSPI Flash — контроль внешней Flash-памяти + +| Параметр | Значение | +| ---------------- | --------- | +| ID | `qspi` | +| Критичный | ✅ Да | +| HIL | ❌ Нет | +| Тип | Self-test | +| Время выполнения | < 500 мс | + +Четыре шага: + +1. **JEDEC ID** — производитель `0xEF` (Winbond), распознавание W25Q64/128/256/512 +2. **Erase + Verify** — стирание последнего сектора, проверка (все байты `0xFF`) +3. **Write + Read + Compare** — 256 байт паттерна `i & 0xFF` +4. **Address range** — только W25Q256/512: проверка dedicated 4-byte opcodes + +```mermaid +sequenceDiagram + participant H as HOST + participant T as TARGET + H->>T: run("qspi") + T->>H: test_begin + Note over T: JEDEC → erase → rw → addr range + T->>H: test_result: pass/fail +``` + +**PASS:** + +```bash +→ {"type":"cmd","cmd":"run","id":"qspi"} +← {"type":"test_begin","id":"qspi","name":"QSPI Flash W25Qxx","critical":true} +← {"type":"test_result","id":"qspi","status":"pass","ms":86,"detail":""} +``` + +**FAIL — чип не отвечает:** + +```bash +← {"type":"test_result","id":"qspi","status":"fail","ms":1, + "detail":"JEDEC: mfr=0xFF exp=0xEF"} +``` + +**FAIL — адресное алиасирование (3-byte wrap на W25Q256/512):** + +```bash +← {"type":"test_result","id":"qspi","status":"fail","ms":312, + "detail":"addr alias: 0x1FFF000 mirrors 0x00FFF000 (3-byte wrap)"} +``` + +--- + +## microSD — контроль SDIO-интерфейса + +| Параметр | Значение | +| ---------------- | ------------------------- | +| ID | `usd` | +| Критичный | ❌ Нет | +| HIL | ❌ Нет | +| Тип | Interactive (pre-confirm) | +| Время выполнения | < 1 с после вставки карты | + +Оператор вставляет карту по запросу. Тест запускается только после подтверждения. +Отказ или таймаут 30 с → `SKIP`. + +Пять шагов с `progress`-событиями: card detect → mount → write 4 KB → read/compare → unmount. + +```mermaid +sequenceDiagram + participant H as HOST + participant T as TARGET + H->>T: run("usd") + T->>H: confirm_request("usd", timeout=30s) + Note over H: оператор вставляет карту + H->>T: confirm("usd", true) + T->>H: test_begin + T->>H: progress: card_detect ok + T->>H: progress: mount ok + T->>H: progress: write ok + T->>H: progress: read_compare ok + T->>H: test_result: pass/fail/skip +``` + +**PASS:** + +```bash +→ {"type":"cmd","cmd":"run","id":"usd"} +← {"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":874,"detail":""} +``` + +**SKIP — оператор нажал Cancel:** + +``` +→ {"type":"confirm","id":"usd","confirmed":false} +← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"operator declined"} +``` + +**SKIP — таймаут 30 с (карта не вставлена, confirm не получен):** + +```bash +← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"confirm timeout"} +``` + +**FAIL — карта не вставлена (confirmed:true, но карты нет в слоте):** + +```bash +← {"type":"test_result","id":"usd","status":"fail","ms":5,"detail":"no card detected"} +``` + +--- + +## TFT-дисплей — визуальная проверка + +| Параметр | Значение | +| ---------------- | ------------------------------------------- | +| ID | `display` | +| Критичный | ❌ Нет | +| HIL | ❌ Нет | +| Тип | Interactive (in-run confirm) | +| Время выполнения | ~1–2 мин (определяется скоростью оператора) | + +**pre-confirm отсутствует** — `test_begin` отправляется сразу после `run`. + +Два этапа, каждый шаг требует подтверждения оператора (таймаут 15 с → FAIL): + +- **Этап 1 (все дисплеи):** Red → Green → Blue → White +- **Этап 2 (TFT7/8/10):** паттерн Red/Blue + горизонтальный флип — диагностика непропаянных LR/UD пинов + +```mermaid +sequenceDiagram + participant H as HOST + participant T as TARGET + H->>T: run("display") + T->>H: test_begin + T->>H: confirm_request(display_red, 15s) + H->>T: confirm(display_red, true) + T->>H: confirm_request(display_green, 15s) + H->>T: confirm(display_green, true) + T->>H: confirm_request(display_blue, 15s) + H->>T: confirm(display_blue, true) + T->>H: confirm_request(display_white, 15s) + H->>T: confirm(display_white, true) + T->>H: confirm_request(display_rot0, 15s) + H->>T: confirm(display_rot0, true) + T->>H: confirm_request(display_rot_base, 15s) + H->>T: confirm(display_rot_base, true) + T->>H: test_result: pass/fail +``` + +**PASS (TFT8 — 6 confirm-шагов):** + +```bash +→ {"type":"cmd","cmd":"run","id":"display"} +← {"type":"test_begin","id":"display","name":"TFT Display RGB888","critical":false} +← {"type":"confirm_request","id":"display_red","prompt":"Screen is solid red?","timeout_ms":15000} +→ {"type":"confirm","id":"display_red","confirmed":true} +← {"type":"confirm_request","id":"display_green","prompt":"Screen is solid green?","timeout_ms":15000} +→ {"type":"confirm","id":"display_green","confirmed":true} +← {"type":"confirm_request","id":"display_blue","prompt":"Screen is solid blue?","timeout_ms":15000} +→ {"type":"confirm","id":"display_blue","confirmed":true} +← {"type":"confirm_request","id":"display_white","prompt":"Screen is solid white?","timeout_ms":15000} +→ {"type":"confirm","id":"display_white","confirmed":true} +← {"type":"confirm_request","id":"display_rot0","prompt":"Screen: left RED, right BLUE?","timeout_ms":15000} +→ {"type":"confirm","id":"display_rot0","confirmed":true} +← {"type":"confirm_request","id":"display_rot_base","prompt":"Left RED and right BLUE swapped sides?","timeout_ms":15000} +→ {"type":"confirm","id":"display_rot_base","confirmed":true} +← {"type":"test_result","id":"display","status":"pass","ms":42310,"detail":""} +``` + +**FAIL — синий экран не подтверждён или таймаут:** + +```bash +← {"type":"test_result","id":"display","status":"fail","ms":15001, + "detail":"display_blue not confirmed"} +``` + +--- + +## Тактовые кнопки — проверка GPIO + +| Параметр | Значение | +| ---------------- | -------------------------------- | +| ID | `buttons` | +| Критичный | ❌ Нет | +| HIL | ❌ Нет | +| Тип | Interactive (физическое нажатие) | +| Время выполнения | до 20 с (2 × 10 с таймаут) | + +| Кнопка | Пин MCU | GPIO | Нажатие | +| ---------- | ---------- | --------- | ----------------- | +| Test_But_1 | GPIO_B1_14 | GPIO2[30] | LOW (pull-up 3V3) | +| Test_But_2 | GPIO_B1_15 | GPIO2[31] | LOW (pull-up 3V3) | + +**Ключевое отличие от других тестов:** хост **не** отправляет `{"type":"confirm",...}`. +`confirm_request` — только UI-подсказка оператору. Прошивка детектирует нажатие +через `bsp_button_get_event_pressed()` с debounce 20 мс. +Таймаут 10 с → `SKIP` (не FAIL). + +```mermaid +sequenceDiagram + participant H as HOST + participant T as TARGET + participant OP as Оператор + H->>T: run("buttons") + T->>H: test_begin + T->>H: confirm_request(btn1_press, timeout=10s) + Note over OP: нажимает Test_But_1 + Note over T: bsp_button детектирует нажатие + T->>H: confirm_request(btn2_press, timeout=10s) + Note over OP: нажимает Test_But_2 + T->>H: test_result: pass/skip +``` + +> **Важно:** после `confirm_request(btn1_press)` от хоста ничего отправлять не нужно. +> Следующий `confirm_request(btn2_press)` придёт сразу после физического нажатия кнопки. + +**PASS:** + +```bash +→ {"type":"cmd","cmd":"run","id":"buttons"} +← {"type":"test_begin","id":"buttons","name":"Test Buttons","critical":false} +← {"type":"confirm_request","id":"btn1_press","prompt":"Press Test_But_1","timeout_ms":10000} + ... оператор нажимает Test_But_1 ... +← {"type":"confirm_request","id":"btn2_press","prompt":"Press Test_But_2","timeout_ms":10000} + ... оператор нажимает Test_But_2 ... +← {"type":"test_result","id":"buttons","status":"pass","ms":8516,"detail":""} +``` + +**SKIP — кнопка не нажата за 10 с:** + +```bash +← {"type":"test_result","id":"buttons","status":"skip","ms":10001, + "detail":"btn1_press timeout"} +``` + +**Типичная ошибка — неверный ID (`button` без `s`):** + +```bash +→ {"type":"cmd","cmd":"run","id":"button"} +← {"ok":false,"error":"UNKNOWN_TEST"} +→ {"type":"cmd","cmd":"run","id":"buttons"} +← {"type":"test_begin","id":"buttons",...} +``` + +--- + +## Запуск всего набора (run_all) + +Тесты запускаются строго в порядке реестра. При провале критичного теста +(`sdram` или `qspi`) все последующие тесты получают `SKIP` с `detail:"critical test failed"`. + +```bash +→ {"type":"cmd","cmd":"run_all"} +← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} +← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} +← {"type":"test_begin","id":"qspi","name":"QSPI Flash W25Qxx","critical":true} +← {"type":"test_result","id":"qspi","status":"pass","ms":86,"detail":""} +← {"type":"confirm_request","id":"usd","prompt":"Insert microSD card and press OK","timeout_ms":30000} + ... оператор вставляет карту и подтверждает ... +← {"type":"test_begin","id":"usd","name":"microSD (SDIO)","critical":false} + ...progress events... +← {"type":"test_result","id":"usd","status":"pass","ms":874,"detail":""} +← {"type":"test_begin","id":"display","name":"TFT Display RGB888","critical":false} + ...confirm цикл 6 шагов... +← {"type":"test_result","id":"display","status":"pass","ms":42310,"detail":""} +← {"type":"test_begin","id":"buttons","name":"Test Buttons","critical":false} +← {"type":"confirm_request","id":"btn1_press","prompt":"Press Test_But_1","timeout_ms":10000} + ... +← {"type":"test_result","id":"buttons","status":"pass","ms":6200,"detail":""} +← {"type":"summary","passed":5,"failed":0,"skipped":0,"overall":"pass"} +``` + +**SKIP-каскад при critical fail:** + +```bash +← {"type":"test_result","id":"sdram","status":"fail","ms":1203,"detail":"addr=0x80200001..."} +← {"type":"test_begin","id":"qspi",...} +← {"type":"test_result","id":"qspi","status":"skip","ms":0,"detail":"critical test failed"} +← {"type":"test_begin","id":"usd",...} +← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"critical test failed"} + ... +← {"type":"summary","passed":0,"failed":1,"skipped":4,"overall":"fail"} +``` + +--- + +## Реестр тестов — порядок выполнения + +| № | ID | Название | Critical | Тип | +| --- | --------- | ------------------ | -------- | ---------------------------- | +| 1 | `sdram` | SDRAM 32 MB | ✅ | Self-test | +| 2 | `qspi` | QSPI Flash W25Qxx | ✅ | Self-test | +| 3 | `usd` | microSD (SDIO) | ❌ | Interactive (pre-confirm) | +| 4 | `display` | TFT Display RGB888 | ❌ | Interactive (in-run confirm) | +| 5 | `buttons` | Test Buttons | ❌ | Interactive (physical) | + +--- + +## Диагностика — строки detail + +| Тест | Значение `detail` | Диагноз | +| --------- | ----------------------------------------------- | ------------------------------------------- | +| `sdram` | `addr=0x... exp=0x.. got=0x..` | Сбой ячейки по адресу | +| `sdram` | `SEMC not ready — DCD failed?` | DCD не инициализировал SEMC | +| `qspi` | `JEDEC: mfr=0xFF exp=0xEF` | Чип не отвечает / не пропаян | +| `qspi` | `JEDEC: unknown cap=0x..` | Неизвестный тип чипа | +| `qspi` | `erase verify failed at 0x...` | Сектор не стирается | +| `qspi` | `rw mismatch at 0x... exp=0x.. got=0x..` | Ошибка записи или чтения | +| `qspi` | `addr alias: 0x... mirrors 0x... (3-byte wrap)` | Dedicated 4-byte opcodes не работают | +| `usd` | `no card detected` | Карта не вставлена в слот | +| `usd` | `mount failed: ` | `f_mount()` вернул FRESULT N | +| `usd` | `write failed: ` | `f_write()` вернул FRESULT N | +| `usd` | `compare failed at offset ` | Данные после чтения не совпадают | +| `display` | `display init failed` | `bsp_display_init()` вернул ошибку | +| `display` | ` not confirmed` | Оператор не подтвердил / истёк таймаут 15 с | +| `buttons` | `btn1_press timeout` | Test_But_1 не нажата за 10 с | +| `buttons` | `btn2_press timeout` | Test_But_2 не нажата за 10 с | +| любой | `confirm timeout` | pre-confirm не получен за 30 с | +| любой | `operator declined` | Получен `"confirmed":false` | +| любой | `critical test failed` | Предшествующий критичный тест провалился | \ No newline at end of file diff --git a/firmware/test/src/tests/test_buttons.c b/firmware/test/src/tests/test_buttons.c new file mode 100644 index 0000000..f3d865c --- /dev/null +++ b/firmware/test/src/tests/test_buttons.c @@ -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 +#include +#include + +/* ── Константы ─────────────────────────────────────────────────────────── */ + +/** @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(¶ms); + + 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, +}; \ No newline at end of file diff --git a/port/README.md b/port/README.md index fe2e972..0d4baf9 100644 --- a/port/README.md +++ b/port/README.md @@ -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/` недоступен на хосте. diff --git a/port/fatfs/README.md b/port/fatfs/README.md new file mode 100644 index 0000000..ee69162 --- /dev/null +++ b/port/fatfs/README.md @@ -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( 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()` | diff --git a/port/log/README.md b/port/log/README.md index 97196fa..b714885 100644 --- a/port/log/README.md +++ b/port/log/README.md @@ -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( 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()` | diff --git a/project_tree.txt b/project_tree.txt deleted file mode 100644 index 4cec5a5..0000000 --- a/project_tree.txt +++ /dev/null @@ -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 diff --git a/utils/README.md b/utils/README.md index d891f07..c23e875 100644 --- a/utils/README.md +++ b/utils/README.md @@ -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( 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). diff --git a/utils/log/README.md b/utils/log/README.md index 5a8a0a6..c0d530c 100644 --- a/utils/log/README.md +++ b/utils/log/README.md @@ -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`) | | Зависимости | ``, ``, `` | +--- + ## Уровни -| 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( PRIVATE utils) +``` + +`LOG_LEVEL` задаётся в пресете или явно: + +```cmake +target_compile_definitions(utils PUBLIC LOG_LEVEL=4) +``` diff --git a/utils/prio_queue/README.md b/utils/prio_queue/README.md index b2a6f94..e4d5096 100644 --- a/utils/prio_queue/README.md +++ b/utils/prio_queue/README.md @@ -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 | нет; при использовании из нескольких контекстов — внешняя синхронизация | | Зависимости | ``, ``, `` | +--- + +## 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` diff --git a/utils/ring_buffer/README.md b/utils/ring_buffer/README.md index 12c4a9c..3071665 100644 --- a/utils/ring_buffer/README.md +++ b/utils/ring_buffer/README.md @@ -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 — только с внешней синхронизацией | | Зависимости | ``, ``, `` | +--- + +## 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`.