- настроен первый HIL-тест (пока без m5stamp; pytest + pyserial + pyocd), правки во всей документации
- начало настройки окружения для отладки проектов firmware/* в devcontainer
This commit is contained in:
Dmitry Akimov 2026-03-19 18:28:45 +03:00
parent 8a96e66e38
commit 8c9e62b3cc
23 changed files with 2997 additions and 730 deletions

View file

@ -22,6 +22,7 @@
"mcu-debug.debug-tracker-vscode", "mcu-debug.debug-tracker-vscode",
"mcu-debug.memory-view", "mcu-debug.memory-view",
"mcu-debug.peripheral-viewer", "mcu-debug.peripheral-viewer",
"marus25.cortex-debug",
"brobeson.ctest-lab", "brobeson.ctest-lab",
"mcu-debug.rtos-views", "mcu-debug.rtos-views",
"ms-vscode.hexeditor", "ms-vscode.hexeditor",
@ -67,6 +68,7 @@
} }
}, },
"runArgs": [ "runArgs": [
"--add-host=host.docker.internal:host-gateway",
"--name", "--name",
"tft-devcontainer" "tft-devcontainer"
] ]

View file

@ -26,4 +26,12 @@ CMAKE_GENERATOR=Ninja
OPENOCD_INTERFACE=cmsis-dap.cfg OPENOCD_INTERFACE=cmsis-dap.cfg
TARGET_CFG=target/imxrt.cfg TARGET_CFG=target/imxrt.cfg
GDB_PORT=3333 GDB_PORT=3333
GDB_EXECUTABLE=gdb-multiarch GDB_EXECUTABLE=gdb-multiarch
# --- HIL (аппаратный стенд) ---
# VCOM-порт MCU-Link (Linux: /dev/ttyACM0, macOS: /dev/cu.usbmodem*)
HIL_VCOM_PORT=/dev/tty.usbmodemGUXFBWDJBWTGQ3
HIL_VCOM_BAUD=115200 # default: 115200
HIL_READY_TIMEOUT=5.0 # default: 5.0 сек
HIL_PYOCD_FREQUENCY=1000000 # default: 1 МГц
№HIL_BUILD_DIR= # default: build/target-debug

96
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,96 @@
{
"version": "0.2.0",
"configurations": [
// =============================================================
// firmware/test без перепрошивки (прошивка уже в flash)
// =============================================================
{
"name": "🐛 Debug: firmware_test",
"type": "cortex-debug",
"request": "launch",
"servertype": "external",
"gdbTarget": "host.docker.internal:3333",
"gdbPath": "arm-none-eabi-gdb",
"executable": "${workspaceFolder}/build/Debug/firmware/test/firmware_test.elf",
"device": "MIMXRT1052CVJ5B",
"svdFile": "${workspaceFolder}/bsp/generated/startup/MIMXRT1052.xml",
"interface": "swd",
"loadFiles": [],
"runToEntryPoint": "main",
"preLaunchTask": "build:firmware-test-debug",
"rttConfig": {
"enabled": true,
"address": "auto",
"clearSearch": false,
"decoders": [
{
"port": 0,
"type": "console",
"label": "RTT"
}
]
}
},
// =============================================================
// firmware/bootloader без перепрошивки
// =============================================================
{
"name": "🐛 Debug: bootloader",
"type": "cortex-debug",
"request": "launch",
"servertype": "external",
"gdbTarget": "host.docker.internal:3333",
"gdbPath": "arm-none-eabi-gdb",
"executable": "${workspaceFolder}/build/Debug/firmware/bootloader/bootloader.elf",
"device": "MIMXRT1052CVJ5B",
"svdFile": "${workspaceFolder}/bsp/generated/startup/MIMXRT1052.xml",
"interface": "swd",
"loadFiles": [],
"runToEntryPoint": "main",
"preLaunchTask": "build:bootloader-debug",
"rttConfig": {
"enabled": true,
"address": "auto",
"clearSearch": false,
"decoders": [
{
"port": 0,
"type": "console",
"label": "RTT"
}
]
}
},
// =============================================================
// firmware/tft_app FreeRTOS task view
// =============================================================
{
"name": "🐛 Debug: tft_app (FreeRTOS)",
"type": "cortex-debug",
"request": "launch",
"servertype": "external",
"gdbTarget": "host.docker.internal:3333",
"gdbPath": "arm-none-eabi-gdb",
"executable": "${workspaceFolder}/build/Debug/firmware/tft_app/app.elf",
"device": "MIMXRT1052CVJ5B",
"svdFile": "${workspaceFolder}/bsp/generated/startup/MIMXRT1052.xml",
"interface": "swd",
"loadFiles": [],
"runToEntryPoint": "main",
"rtos": "FreeRTOS",
"preLaunchTask": "build:app-debug",
"rttConfig": {
"enabled": true,
"address": "auto",
"clearSearch": false,
"decoders": [
{
"port": 0,
"type": "console",
"label": "RTT"
}
]
}
}
]
}

83
.vscode/tasks.json vendored
View file

@ -65,6 +65,29 @@
"problemMatcher": [] "problemMatcher": []
}, },
// ============================================================= // =============================================================
// HIL TARGET-ТЕСТЫ
// =============================================================
{
"label": "🎯 Build HIL Target Tests",
"group": "build",
"type": "shell",
"command": "just",
"args": [
"build::build-hil"
],
"options": {
"cwd": "${workspaceFolder}",
"statusbar": {
"color": "#00e5ff",
"label": "$(circuit-board) HIL Build",
"detail": "Build HIL target firmware"
}
},
"problemMatcher": [
"$gcc"
]
},
// =============================================================
// HAB-ОБРАЗЫ // HAB-ОБРАЗЫ
// ============================================================= // =============================================================
{ {
@ -131,6 +154,66 @@
} }
}, },
"problemMatcher": [] "problemMatcher": []
},
// =============================================================
// СБОРКА ДЛЯ ОТЛАДКИ (preLaunchTask без интерактивных inputs)
// =============================================================
{
"label": "build:firmware-test-debug",
"type": "shell",
"hide": true,
"command": "just",
"args": [
"build::build-firmware-test-debug"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"presentation": {
"reveal": "silent",
"panel": "shared"
}
},
{
"label": "build:bootloader-debug",
"hide": true,
"type": "shell",
"command": "just",
"args": [
"build::build-bootloader-debug"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"presentation": {
"reveal": "silent",
"panel": "shared"
}
},
{
"label": "build:app-debug",
"hide": true,
"type": "shell",
"command": "just",
"args": [
"build::build-app-debug"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"presentation": {
"reveal": "silent",
"panel": "shared"
}
} }
], ],
"inputs": [ "inputs": [

View file

@ -13,9 +13,19 @@ option(UNITY_TESTING_ENABLED "Enable Unity framework" OFF)
# Общие настройки для ARM сборки # Общие настройки для ARM сборки
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
if(NOT BUILD_TESTS_HOST) if(NOT BUILD_TESTS_HOST)
set(CMAKE_EXECUTABLE_SUFFIX ".elf")
set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF) set(CMAKE_C_EXTENSIONS OFF)
set(BSP_SYSCALLS_FILE
"${CMAKE_SOURCE_DIR}/bsp/generated/syscalls.c"
CACHE FILEPATH "Заглушки системных вызовов newlib")
set(BSP_GENERATED
"${CMAKE_SOURCE_DIR}/bsp/generated"
CACHE PATH "Путь до сгенерированных ConfigTools файлов")
set(BSP_STARTUP_FILE
"${BSP_GENERATED}/startup/startup_MIMXRT1052.S"
CACHE FILEPATH "Путь до стартап файла")
endif() endif()
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View file

@ -45,22 +45,18 @@ default:
# === Популярные алиасы (для удобства команды) === # === Популярные алиасы (для удобства команды) ===
# Сокращают длинные вызовы модулей # Сокращают длинные вызовы модулей
# Инициализация окружения (эквивалент host::bootstrap) [doc('Первичная настройка окружения после git clone (на хосте)')]
init: init:
@just host::bootstrap @just host::bootstrap
# Быстрая прошивка тестового образа в Debug [doc('Прошить firmware_test Debug во Flash через USB ROM')]
flash: flash:
@just host::flash-test-debug @just host::flash-test-debug
# Полная сборка всех проектов в Release (в контейнере) [doc('Собрать все HAB-образы в Release')]
build-all: build-all:
@just build::hab-all-release @just build::hab-all-release
# CI пайплайн [doc('Запустить CI pipeline')]
run-ci: run-ci:
@just ci::pipeline @just ci::pipeline

195
README.md
View file

@ -17,60 +17,78 @@
│ ├── launch.json │ ├── launch.json
│ └── tasks.json # UI для just build::* (внутри devcontainer) │ └── tasks.json # UI для just build::* (внутри devcontainer)
├── bsp/ # Board Support Package ├── bsp/ # Board Support Package
│ └── generated/ # Сгенерировано NXP Config Tools (Pins + Clocks Tool) │ ├── CMakeLists.txt
│ ├── TFT_Board.mex # Источник истины конфигурации пинов и тактирования │ ├── common/ # Общие типы (bsp_status_t и др.)
│ ├── pin_mux.c/h # Сгенерировано из .mex (Pins Tool) │ ├── generated/ # Сгенерировано NXP Config Tools (Pins + Clocks Tool)
│ ├── clock_config.c/h # Сгенерировано из .mex (Clocks Tool) │ │ ├── TFT_Board.mex # Источник истины конфигурации пинов и тактирования
│ ├── board.c/h # Ручная инициализация специфики платы │ │ ├── pin_mux.c/h # Сгенерировано из .mex (Pins Tool)
│ └── BOOT_FLAGS.md # Описание флагов загрузчика │ │ ├── clock_config.c/h # Сгенерировано из .mex (Clocks Tool)
├── cmake/ # Общие CMake модули и toolchain files │ │ ├── board.c/h # Ручная инициализация специфики платы
│ ├── linker/ # Линкер-скрипты под разные схемы размещения │ │ ├── syscalls.c # Заглушки системных вызовов newlib
│ │ └── startup/ # Стартап-файл для ARM
│ ├── led/ # bsp_led — два UserLed (GPIO3_IO03, GPIO3_IO04)
│ ├── tick/ # bsp_tick — SysTick / FreeRTOS-совместимый таймер
│ ├── uart_host/ # bsp_uart_host — LPUART1 (MCU-Link VCOM, J2)
│ │ └── mocks/ # Мок-реализация для host-тестов
│ └── usb_cdc/ # bsp_usb_cdc — USB CDC ACM
├── cmake/ # Общие CMake модули
│ ├── linker/ # Линкер-скрипты (ram, flexspi_nor, sdram и др.)
│ ├── toolchain_arm.cmake # ARM cross-compilation toolchain │ ├── toolchain_arm.cmake # ARM cross-compilation toolchain
│ └── toolchain_host.cmake # Host GCC для unit-тестов │ └── toolchain_host.cmake # Host GCC для unit-тестов
├── sdk/ # NXP MCUXpresso SDK — vendored ├── sdk/ # NXP MCUXpresso SDK — vendored
│ ├── CMakeLists.txt ├── lib/ # Внешние библиотеки — vendored
│ ├── CMSIS/ │ ├── Unity/ # Фреймворк для unit-тестов
│ ├── devices/MIMXRT1052/ # Драйверы, startup, утилиты │ ├── fff/ # Fake Function Framework для моков
│ ├── components/ # fsl_button, fsl_led, serial_manager и др.
│ ├── middleware/ # FatFS, FreeRTOS, LittleFS, USB, mcuboot и др.
│ └── rtos/freertos/ # FreeRTOS (vendored через SDK)
├── lib/ # Внешние библиотеки
│ ├── Unity/ # Фреймворк для unit-тестов (vendored)
│ ├── fff/ # Fake Function Framework для моков (vendored)
│ └── SEGGER/ # SEGGER RTT — вывод логов через отладчик │ └── SEGGER/ # SEGGER RTT — вывод логов через отладчик
├── firmware/ ├── firmware/
│ ├── test/ # [Проект 1] Тестовая прошивка — входной контроль платы │ ├── test/ # [Проект 1] Тестовая прошивка — входной контроль платы
│ ├── bootloader/ # [Проект 2] Загрузчик с поддержкой A/B обновления │ ├── bootloader/ # [Проект 2] Загрузчик с поддержкой A/B обновления
│ └── tft_app/ # [Проект 3] Основная боевая прошивка (FreeRTOS) │ └── tft_app/ # [Проект 3] Основная боевая прошивка (FreeRTOS)
├── tests/ # Тесты (host + target) ├── tests/
│ ├── host/ # Unit/интеграционные тесты, запускаемые на хосте │ ├── CMakeLists.txt
│ ├── target/ # Тесты периферии, запускаемые на таргете │ ├── host/ # Unit-тесты на хостовом компиляторе (Unity + fff)
│ ├── HostTestingGuide.md │ │ ├── mocks/ # Stub-хедеры NXP SDK для компиляции на хосте
│ └── README.md │ │ ├── led/ # Тесты bsp_led
│ │ ├── ring_buffer/ # Тесты ring_buffer
│ │ ├── timeout/ # Тесты таймаут-паттерна
│ │ └── uart_host/ # Тесты bsp_uart_host (через мок)
│ ├── target/ # HIL target-прошивки (загружаются в RAM через pyOCD)
│ │ └── host_uart/ # CLI-прошивка для тестирования bsp_uart_host
│ ├── HOST_CREATE_TEST.md # Гайд: добавление host-теста
│ └── HIL_CREATE_TEST.md # Гайд: добавление HIL-теста
├── tools/ ├── tools/
│ └── host/ # Инструменты для работы с таргетом │ ├── host/ # Инструменты прошивки (spsdk)
│ ├── flash_usb.py # Прошивка через USB ROM (nxp-spsdk / blhost) │ │ ├── flash_usb.py # Прошивка через USB ROM (sdphost + blhost)
│ ├── hab/ # Утилиты и гайд по HAB (Secure Boot) │ │ ├── hab/ # HAB yaml-конфиги для nxpimage
│ ├── dcd/ # Device Configuration Data │ │ ├── dcd/ # ivt_flashloader.bin, dcd.bin
│ ├── pyproject.toml │ │ ├── pyproject.toml
│ └── uv.lock │ │ └── uv.lock
├── just/ # Just-модули (автоматизация) │ └── hil/ # HIL-тесты (pytest + pyOCD + pyserial)
│ ├── build.just # Сборка, тесты, HAB-образы (devcontainer) │ ├── pyproject.toml
│ ├── host.just # Прошивка, bootstrap, HIL (хост) │ ├── conftest.py # Фикстуры: загрузка ELF + UART
│ ├── pyocd_utils.py # FLEXRAM init, ELF loader, run_from_vectors
│ ├── env_config.py # Конфигурация из os.environ / .env
│ ├── load_and_run.py # CLI-утилита для ручной загрузки ELF в RAM микроконтроллера
│ └── test_uart.py # Тесты bsp_uart_host (PING/ECHO/BUF_SIZE)
├── utils/
│ └── ring_buffer/ # Платформонезависимый кольцевой буфер
├── just/
│ ├── build.just # devcontainer: сборка, тесты, HAB, HIL-прошивки
│ ├── host.just # хост: прошивка, bootstrap, HIL-запуск
│ └── ci.just # CI/CD пайплайны │ └── ci.just # CI/CD пайплайны
├── docs/ # Документация проекта ├── docs/
│ ├── DEV_ARCH.md # Архитектура окружения разработки │ ├── DEV_ARCH.md
│ ├── CMAKE_HINTS.md # Шпаргалка по CMake в проекте │ ├── CMAKE_HINTS.md
│ ├── schematic.pdf # Схема платы │ ├── HOW_TO_FLASH.md
│ ├── mcu_rm.pdf # Reference Manual IMXRT1052 │ ├── BOOT_FLAGS.md # Флаги загрузчика
│ └── manufacturing_user's_guide.pdf │ └── schematic.pdf # Схема платы
├── .env # Конфигурация проекта (VID:PID, пути, GDB и др.) ├── pyocd.yaml # Конфигурация pyOCD (target: cortex_m, RAM-режим)
├── .env # Конфигурация проекта (VID:PID, HIL-порты и др.)
├── .env.example # Шаблон .env для новых разработчиков ├── .env.example # Шаблон .env для новых разработчиков
├── bootstrap.sh # Первичная настройка окружения (уровень 0) ├── bootstrap.sh # Первичная настройка окружения (уровень 0)
├── CMakeLists.txt # Корневой CMake ├── CMakeLists.txt # Корневой CMake
├── CMakePresets.json # Пресеты сборки (Release/Debug/Host) ├── CMakePresets.json # Пресеты сборки (Debug/Release/Host/Target)
├── justfile # Точка входа для команд (модули: build, host, ci) └── justfile # Точка входа для команд (модули: build, host, ci)
└── README.md
``` ```
--- ---
@ -81,13 +99,11 @@
Bare-metal прошивка для **входного контроля** платы. Проверяет базовую работоспособность всех интерфейсов: CAN, UART, SDRAM, QSPI Flash, uSD (SDIO), RGB-интерфейс, гальванически развязанные входы, светодиоды, кнопки, IR-приёмник, MQS. Bare-metal прошивка для **входного контроля** платы. Проверяет базовую работоспособность всех интерфейсов: CAN, UART, SDRAM, QSPI Flash, uSD (SDIO), RGB-интерфейс, гальванически развязанные входы, светодиоды, кнопки, IR-приёмник, MQS.
Загружается через USB ROM (Serial Download Mode) — подробнее в [HOW_TO_FLASH.md](HOW_TO_FLASH.md). Загружается через USB ROM (SDP) — подробнее в [docs/HOW_TO_FLASH.md](docs/HOW_TO_FLASH.md).
> **Рекомендуется начать разработку с этого проекта** — он наиболее прост и позволяет полностью отладить окружение сборки и прошивки.
### 2. Загрузчик (`firmware/bootloader/`) ### 2. Загрузчик (`firmware/bootloader/`)
Отвечает за обновление боевой прошивки в полевых условиях. Поддерживает схему **A/B** с обновлением через uSD. Обновление самого загрузчика — только через внешний инструмент (USB ROM + blhost), не через себя. На производстве загружается единым blob-ом вместе с первой версией боевой прошивки. Отвечает за обновление боевой прошивки в полевых условиях. Поддерживает схему **A/B** с обновлением через uSD. Обновление самого загрузчика — только через USB ROM + blhost, не через себя.
### 3. Боевая прошивка (`firmware/tft_app/`) ### 3. Боевая прошивка (`firmware/tft_app/`)
@ -97,50 +113,60 @@ Bare-metal прошивка для **входного контроля** пла
## Тестирование ## Тестирование
Стратегия тестирования двухуровневая: Стратегия тестирования трёхуровневая:
| Уровень | Расположение | Инструменты | Запуск | | Уровень | Расположение | Инструменты | Запуск |
| -------------------------------------- | --------------- | --------------- | -------------------------------------- | |---------|-------------|-------------|--------|
| **Host-тесты** (unit + интеграционные) | `tests/host/` | Unity + fff | `just build::test-host` в devcontainer | | **Host-тесты** (unit) | `tests/host/` | Unity + fff | `just build::test-host` в devcontainer |
| **Target-тесты** (аппаратные) | `tests/target/` | Unity на железе | Удалённый ПК-сервер через SSH | | **HIL target-тесты** (аппаратные) | `tests/target/` + `tools/hil/` | pyOCD + pyserial + pytest | `just host::hil-run` на хосте |
Подробнее — [tests/HostTestingGuide.md](tests/HostTestingGuide.md) и [tests/README.md](tests/README.md). ### Host-тесты
Компилируются и выполняются в devcontainer на хостовом компиляторе. Железо не нужно. BSP-модули тестируются через fff-фейки и stub-хедеры из `tests/host/mocks/`.
Гайд по добавлению нового теста — [tests/HOST_CREATE_TEST.md](tests/HOST_CREATE_TEST.md).
### HIL target-тесты
Каждый HIL-тест — это пара: **C-прошивка** (`tests/target/<n>/`) с текстовым CLI через UART и **pytest-тесты** (`tools/hil/test_<n>.py`). pyOCD загружает `.elf` в RAM через MCU-Link (CMSIS-DAP), pytest общается с прошивкой через MCU-Link VCOM.
```bash
pytest → uart_cmd("PING\r\n") → MCU-Link VCOM → RT1052 → "PONG\r\n" → pytest
```
Гайд по добавлению нового теста — [tests/HIL_CREATE_TEST.md](tests/HIL_CREATE_TEST.md).
--- ---
## Управление зависимостями ## Управление зависимостями
| Зависимость | Подход | Причина | | Зависимость | Подход | Причина |
| ------------------------------- | -------------------- | -------------------------------------------- | |-------------|--------|---------|
| NXP MCUXpresso SDK | vendored | Стабильная версия, обновлений не планируется | | NXP MCUXpresso SDK | vendored | Стабильная версия, обновлений не планируется |
| FreeRTOS, FatFS, LittleFS и др. | vendored (через SDK) | Стабильные версии | | FreeRTOS, FatFS, LittleFS и др. | vendored (через SDK) | Стабильные версии |
| Unity + fff | vendored | Маленькие, стабильные | | Unity + fff | vendored | Маленькие, стабильные |
| SEGGER RTT | vendored | Стабильный | | SEGGER RTT | vendored | Стабильный |
| pyOCD, pyserial, pytest | `tools/hil/uv.lock` | Фиксированные версии |
| spsdk (nxpimage, blhost) | `tools/host/uv.lock` | Фиксированные версии |
**Принцип:** всё что не меняется — vendored (закоммичено в репозиторий). Это обеспечивает полностью автономную сборку после `git clone` без доступа к интернету. **Принцип:** всё что не меняется — vendored. Полностью автономная сборка после `git clone` без доступа к интернету (кроме Python-зависимостей).
--- ---
## Devcontainer — состав окружения ## Devcontainer — состав окружения
| Инструмент | Назначение | | Инструмент | Назначение |
| ---------------------- | -------------------------------------- | |------------|------------|
| `arm-none-eabi-gcc` | Сборка firmware для таргета | | `arm-none-eabi-gcc` | Сборка firmware и HIL target-прошивок для ARM |
| `arm-none-eabi-gdb` | Отладка через GDB server (удалённая) | | `arm-none-eabi-gdb` | Отладка через GDB server |
| `gcc` (host) | Сборка и запуск host-тестов | | `gcc` / `clang` (host) | Сборка и запуск host-тестов |
| `CMake + Ninja` | Система сборки | | `CMake + Ninja` | Система сборки |
| `CTest` | Запуск тестов (Unity + fff) | | `CTest` | Запуск host-тестов |
| `clangd` | Language server для VSCode | | `clangd` | Language server для VSCode |
| `clang-format` | Форматирование кода | | `clang-format` | Форматирование кода |
| `clang-tidy` | Статический анализ | | `clang-tidy` | Статический анализ |
| `Python 3 + nxp-spsdk` | HAB-образы (nxpimage) | | `Python 3 + nxp-spsdk` | HAB-образы (nxpimage) |
| `just` | Запуск рецептов через модули `build::` | | `just` | Запуск рецептов через модули `build::` |
---
## Конфигурация платы (NXP Config Tools)
Файл `bsp/generated/TFT_Board.mex`**источник истины** для конфигурации пинов и тактирования. Открывается в NXP Config Tools (Pins Tool + Clocks Tool) для регенерации `pin_mux.c/h` и `clock_config.c/h`. Используется при старте проекта или при изменении аппаратной схемы. Коммитится вместе со сгенерированным кодом.
--- ---
@ -151,19 +177,16 @@ git clone <repo-url>
cd tft_manufacture_test cd tft_manufacture_test
# Инициализация хоста (один раз) # Инициализация хоста (один раз)
# Устанавливает just и uv, затем настраивает окружение
./bootstrap.sh ./bootstrap.sh
# Открыть в VSCode → Reopen in Container # Открыть в VSCode → Reopen in Container
# Затем внутри devcontainer: # Затем внутри devcontainer:
just build::test-host # host unit-тесты
just build::test-host # сборка и запуск host-тестов just build::build-firmware-test-debug # сборка firmware
just build::build-firmware-test-debug # сборка firmware для таргета just build::hab-firmware-test-debug # подготовка HAB-образа
just build::hab-firmware-test-debug # подготовка HAB-образа just build::build-hil # сборка HIL target-прошивок
# На хосте (вне контейнера): # На хосте (вне контейнера):
just flash ... # прошивка через USB ROM just flash # прошить firmware_test debug во Flash
just host::hil-run # загрузить HIL ELF + запустить pytest
``` ```
> Подробнее о прошивке — [HOW_TO_FLASH.md](docs/HOW_TO_FLASH.md)
> Подробнее об окружении разработки — [docs/DEV_ARCH.md](docs/DEV_ARCH.md)

View file

@ -18,6 +18,7 @@
/* BSP */ /* BSP */
#include "bsp/tick.h" #include "bsp/tick.h"
#include "clock_config.h"
/* Utils */ /* Utils */
#include "ring_buffer/ring_buffer.h" #include "ring_buffer/ring_buffer.h"
@ -35,12 +36,9 @@
#define BSP_UART_HOST_RX_BUFFER_SIZE (256U) #define BSP_UART_HOST_RX_BUFFER_SIZE (256U)
#endif #endif
/* Частота источника тактирования LPUART1. /* Частота источника тактирования LPUART1 (NXP Config Tools) */
* BOARD_BootClockRUN() -> 80000000UL настраивает OSC 24 MHz на LPUART.
* Скорректируй если у вас другой clock source. */
#ifndef BSP_UART_HOST_SRC_CLOCK_HZ #ifndef BSP_UART_HOST_SRC_CLOCK_HZ
//TODO: брать из generated/clock_config.h! #define BSP_UART_HOST_SRC_CLOCK_HZ BOARD_BOOTCLOCKRUN_UART_CLK_ROOT
#define BSP_UART_HOST_SRC_CLOCK_HZ (80000000UL)
#endif #endif
/* Приоритет прерывания LPUART1 (0 = наивысший на CM7). */ /* Приоритет прерывания LPUART1 (0 = наивысший на CM7). */

View file

@ -9,9 +9,6 @@ else()
set(TOOLCHAIN_EXT "") set(TOOLCHAIN_EXT "")
endif() endif()
# EXECUTABLE EXTENSION
set(CMAKE_EXECUTABLE_SUFFIX ".elf")
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Путь к тулчейну В devcontainer: /opt/arm-toolchain (symlink на конкретную # Путь к тулчейну В devcontainer: /opt/arm-toolchain (symlink на конкретную
# версию) Переопределяется через переменную окружения ARMGCC_DIR если нужно # версию) Переопределяется через переменную окружения ARMGCC_DIR если нужно

View file

@ -1,9 +1,8 @@
# Архитектура рабочего окружения разработчика # Архитектура рабочего окружения разработчика
> Проект: TFT Firmware (MIMXRT1052CVJ5B) > Проект: TFT Firmware (MIMXRT1052CVJ5B)
> Документ описывает итоговый рабочий процесс разработчиков: от разворачивания > Документ описывает рабочий процесс разработчиков: разворачивание окружения,
> окружения до сборки, тестирования, отладки и прошивки платы. > сборка, тестирование (host + HIL), отладка и прошивка платы.
> Производственный сервер описан кратко — подробно в отдельном документе.
--- ---
@ -12,15 +11,12 @@
Рабочее окружение разделено на два контекста с чёткой границей: Рабочее окружение разделено на два контекста с чёткой границей:
**Devcontainer** — всё что касается кода: сборка, статический анализ, **Devcontainer** — всё что касается кода: сборка, статический анализ,
форматирование, host-тесты, подготовка HAB-образов. Разработчик проводит форматирование, host-тесты, сборка HIL target-прошивок, подготовка HAB-образов.
здесь большую часть времени. Управляется через VSCode tasks и модуль `just build::`. Управляется через VSCode tasks и модуль `just build::`.
**Хост** — всё что касается железа: прошивка платы через USB, отладка **Хост** — всё что касается железа: прошивка платы через USB, HIL-тесты
через JLink/probe-rs. Управляется через модуль `just host::`. через pyOCD + pytest, отладка через GDB-сервер.
Управляется через модуль `just host::`.
Такое разделение решает несколько проблем: USB-устройства не требуют
проброса в контейнер; оба разработчика работают в идентичных условиях;
CI использует те же команды что и локальная разработка.
--- ---
@ -32,57 +28,56 @@ CI использует те же команды что и локальная р
├── Хост (Linux / macOS / Windows + Git Bash) ├── Хост (Linux / macOS / Windows + Git Bash)
│ ├── just ← запуск задач хостового уровня (just host::*) │ ├── just ← запуск задач хостового уровня (just host::*)
│ ├── docker ← управление devcontainer │ ├── docker ← управление devcontainer
│ ├── git ← работа с репозиторием
│ ├── uv + spsdk ← прошивка платы (flash_usb.py, sdphost, blhost) │ ├── uv + spsdk ← прошивка платы (flash_usb.py, sdphost, blhost)
│ │ venv: tools/host/.venv. (Linux/macOS) │ │ venv: tools/host/.venv-host (Linux/macOS)
│ │ tools/host/.venv-host-win (Windows) │ │ tools/host/.venv-host-win (Windows)
│ ├── uv + pyocd ← HIL-тесты (tools/hil/.venv)
│ │ + pyserial
│ │ + pytest
│ ├── JLinkGDBServer / probe-rs ← сервер отладки (USB → TCP :2331) │ ├── JLinkGDBServer / probe-rs ← сервер отладки (USB → TCP :2331)
│ └── VSCode ← IDE (Dev Containers extension) │ └── VSCode ← IDE (Dev Containers extension)
├── Devcontainer (Docker) ├── Devcontainer (Docker)
│ ├── ARM GCC 13.3 ← кросс-компилятор │ ├── ARM GCC 13.3 ← кросс-компилятор (firmware + HIL target-прошивки)
│ ├── cmake + ninja ← система сборки │ ├── cmake + ninja ← система сборки
│ ├── clang-17 ← компилятор для host-тестов │ ├── clang-17 ← компилятор для host-тестов
│ ├── clangd-17 ← LSP (автодополнение, диагностика) │ ├── clangd-17 ← LSP (автодополнение, диагностика)
│ ├── clang-tidy-17 ← статический анализ │ ├── clang-tidy-17 ← статический анализ
│ ├── clang-format-17 ← форматирование кода │ ├── clang-format-17 ← форматирование кода
│ ├── cmake-format ← форматирование CMakeLists
│ ├── just ← запуск задач внутри контейнера (just build::*) │ ├── just ← запуск задач внутри контейнера (just build::*)
│ ├── uv + spsdk ← сборка HAB-образов (только nxpimage) │ ├── uv + spsdk ← сборка HAB-образов (только nxpimage)
│ │ venv: tools/host/.venv
│ └── Unity + fff ← фреймворки host-тестов │ └── Unity + fff ← фреймворки host-тестов
└── Плата TFT (IMXRT1052) — на столе у разработчика └── Плата TFT (IMXRT1052)
├── USB ──────────────▶ хост (SDP-режим, прошивка) ├── USB ──────────────────▶ хост (SDP-режим, прошивка)
├── SWD ──────────────▶ JLink/probe-rs на хосте (отладка) ├── NXP MCU-Link (USB) ───▶ хост (CMSIS-DAP SWD + VCOM)
└── CAN / UART / IO ──▶ локальный стенд (target-тесты) │ ├── SWD ← pyOCD загружает HIL ELF в RAM
│ └── VCOM ← pytest общается с прошивкой через UART
├── SWD ──────────────────▶ JLink/probe-rs на хосте (отладка)
└── CAN / UART / IO ──────▶ локальный стенд (будущие HIL-тесты)
``` ```
--- ---
## 3. Что устанавливается и где ## 3. Что устанавливается и где
| Инструмент | Хост | Devcontainer | Сервер | | Инструмент | Хост | Devcontainer | Сервер |
| ------------------ | --------- | ------------ | --------- | |------------|------|--------------|--------|
| `just` | ✅ | ✅ Dockerfile | ✅ | | `just` | ✅ | ✅ Dockerfile | ✅ |
| `docker` | ✅ | — | — | | `docker` | ✅ | — | — |
| `git` | ✅ | ✅ | ✅ | | `uv` | ✅ | ✅ Dockerfile | ✅ |
| `uv` | ✅ | ✅ Dockerfile | ✅ | | `spsdk` | ✅ uv sync | ✅ uv sync | ✅ uv sync |
| `spsdk` | ✅ uv sync | ✅ uv sync | ✅ uv sync | | `pyocd` + `pyserial` + `pytest` | ✅ uv sync | — | ✅ uv sync |
| ARM GCC toolchain | — | ✅ | — | | ARM GCC toolchain | — | ✅ | — |
| `cmake` / `ninja` | — | ✅ | — | | `cmake` / `ninja` | — | ✅ | — |
| `clang` / `clangd` | — | ✅ | — | | `clang` / `clangd` | — | ✅ | — |
| Unity / fff | — | ✅ | — | | Unity / fff | — | ✅ | — |
| JLink / probe-rs | ✅ | — | — | | JLink / probe-rs | ✅ | — | — |
`spsdk` присутствует везде, но с разными ролями: `spsdk` и `pyocd` — разные venv с разными ролями:
- **devcontainer** — только `nxpimage` для сборки HAB-образов - `tools/host/``spsdk`: `nxpimage` + `sdphost` + `blhost` — прошивка через USB ROM
- **хост** — полный стек: `nxpimage` + `sdphost` + `blhost` для прошивки - `tools/hil/``pyocd` + `pyserial` + `pytest` — HIL-тесты через SWD + VCOM
- **сервер** — то же что на хосте, но для производственного сценария
Версия `spsdk` зафиксирована в `tools/host/uv.lock` — все три места
используют одну и ту же версию.
--- ---
@ -92,7 +87,7 @@ CI использует те же команды что и локальная р
```ini ```ini
# USB VID:PID (NXP) # USB VID:PID (NXP)
BOOTROM_VID=1fc9 # используется: host.just (udev-правила) + flash_usb.py BOOTROM_VID=1fc9
BOOTROM_PID=0130 BOOTROM_PID=0130
FLASHLOADER_VID=15a2 FLASHLOADER_VID=15a2
FLASHLOADER_PID=0073 FLASHLOADER_PID=0073
@ -102,6 +97,12 @@ GDB_PORT=3333
GDB_EXECUTABLE=gdb-multiarch GDB_EXECUTABLE=gdb-multiarch
OPENOCD_INTERFACE=cmsis-dap.cfg OPENOCD_INTERFACE=cmsis-dap.cfg
TARGET_CFG=target/imxrt.cfg TARGET_CFG=target/imxrt.cfg
# HIL — аппаратный стенд
HIL_VCOM_PORT=/dev/tty.usbmodemXXXX # VCOM-порт MCU-Link (macOS/Linux)
# HIL_VCOM_BAUD=115200 # default: 115200
# HIL_READY_TIMEOUT=5.0 # default: 5.0 сек
# HIL_PYOCD_FREQUENCY=1000000 # default: 1 МГц
``` ```
**Как значения попадают в инструменты:** **Как значения попадают в инструменты:**
@ -110,64 +111,54 @@ TARGET_CFG=target/imxrt.cfg
.env .env
├─▶ just (set dotenv-load + set export) ├─▶ just (set dotenv-load + set export)
│ └─▶ just-рецепты видят переменные напрямую: {{BOOTROM_VID}} │ ├─▶ just-рецепты: {{BOOTROM_VID}}, {{HIL_VCOM_PORT}}
│ └─▶ uv run flash_usb.py ← наследует окружение автоматически │ └─▶ uv run python ← наследует os.environ автоматически
│ └─▶ os.environ.get("BOOTROM_VID") ← без python-dotenv │ ├─▶ flash_usb.py: os.environ.get("BOOTROM_VID")
│ └─▶ env_config.py: os.environ.get("HIL_VCOM_PORT")
└─▶ .vscode/launch.json ← через ${env:GDB_PORT} если нужно └─▶ .vscode/launch.json ← через ${env:GDB_PORT}
``` ```
Аппаратные константы (`FLASH_BASE`, `HAB_OFFSET`, `FLEXSPI_OPTION_VALUE` и т.д.)
намеренно оставлены в коде `flash_usb.py` — это не конфигурация, а часть
протокола прошивки IMXRT1052, менять их незачем.
`.env.example` — шаблон без значений, коммитится в репозиторий. `.env.example` — шаблон без значений, коммитится в репозиторий.
`.env` — реальные значения, в `.gitignore` (если содержит секреты), иначе тоже коммитится.
--- ---
## 4. Структура репозитория (automation-часть) ## 4. Структура automation-части репозитория
```bash ```bash
/ /
├── justfile ← корневой оркестратор; модули: build, host, ci ├── justfile ← корневой оркестратор; модули: build, host, ci
├── bootstrap.sh ← уровень 0: устанавливает just+uv → just host::bootstrap ├── bootstrap.sh ← уровень 0: устанавливает just+uv → just host::bootstrap
├── .env ← конфигурация проекта (VID:PID, пути, GDB) ├── pyocd.yaml ← конфигурация pyOCD (target: cortex_m, RAM-режим)
├── .env.example ← шаблон для новых разработчиков ├── .env / .env.example
├── just/ ├── just/
│ ├── build.just ← devcontainer: сборка, тесты, HAB │ ├── build.just ← devcontainer: сборка firmware, host-тесты,
│ ├── host.just ← хост: прошивка, bootstrap, HIL │ │ HAB-образы, HIL target-прошивки
│ └── ci.just ← CI/CD пайплайны │ ├── host.just ← хост: прошивка, bootstrap, HIL-тесты
│ └── ci.just
├── .devcontainer/ ├── .devcontainer/
│ ├── Dockerfile │ ├── Dockerfile
│ └── devcontainer.json │ └── devcontainer.json
├── .vscode/ ├── .vscode/
│ └── tasks.json ← UI для just build::* (внутри devcontainer) │ └── tasks.json ← UI для just build::* (внутри devcontainer)
├── tools/ ├── tools/
│ └── host/ │ ├── host/ ← spsdk-окружение (прошивка через USB ROM)
│ ├── hab/ │ │ ├── flash_usb.py
│ │ ├── hab_firmware_test_debug.yaml │ │ ├── hab/ ← HAB yaml-конфиги
│ │ ├── hab_firmware_test_release.yaml │ │ ├── dcd/ ← ivt_flashloader.bin, dcd.bin
│ │ ├── hab_bootloader_debug.yaml │ │ └── uv.lock
│ │ ├── hab_bootloader_release.yaml │ └── hil/ ← HIL pytest-окружение
│ │ ├── hab_app_debug.yaml │ ├── conftest.py ← фикстуры: loaded_<n>, uart
│ │ └── hab_app_release.yaml │ ├── pyocd_utils.py ← FLEXRAM, ELF loader, run_from_vectors
│ ├── dcd/ │ ├── env_config.py ← конфигурация из os.environ
│ │ ├── dcd.bin │ ├── load_and_run.py ← CLI-утилита загрузки ELF
│ │ └── ivt_flashloader.bin │ ├── test_uart.py ← HIL тесты bsp_uart_host
│ ├── flash_usb.py ← читает конфиг из env (VID:PID, BUILD_DIR)
│ ├── pyproject.toml
│ └── uv.lock │ └── uv.lock
├── CMakePresets.json ├── CMakePresets.json ← Debug · Release · host-debug · target-debug
├── cmake/ └── tests/
├── sdk/ ├── host/ ← host unit-тесты (Unity + fff)
├── bsp/ └── target/ ← HIL target-прошивки (RAM, pyOCD)
├── lib/ └── host_uart/ ← CLI для тестирования UART
├── firmware/
│ ├── test/
│ ├── bootloader/
│ └── tft_app/
└── tests/ ← host-тесты (Unity + fff)
``` ```
--- ---
@ -176,13 +167,13 @@ TARGET_CFG=target/imxrt.cfg
### 5.1 Предварительные требования ### 5.1 Предварительные требования
| Платформа | Что нужно до bootstrap | | Платформа | Что нужно до bootstrap |
| --------- | ----------------------------------------------------------- | |-----------|------------------------|
| Linux | `docker`, `git`, `curl` | | Linux | `docker`, `git`, `curl` |
| macOS | Docker Desktop, `git` (Xcode CLT) | | macOS | Docker Desktop, `git` (Xcode CLT) |
| Windows | Docker Desktop, Git for Windows → **использовать Git Bash** | | Windows | Docker Desktop, Git for Windows → **Git Bash** |
### 5.2 Единственная команда для нового разработчика ### 5.2 Единственная команда
```bash ```bash
git clone <repo-url> && cd <repo> git clone <repo-url> && cd <repo>
@ -195,107 +186,81 @@ git clone <repo-url> && cd <repo>
bootstrap.sh (уровень 0) bootstrap.sh (уровень 0)
├── определить платформу (Linux / macOS / Windows Git Bash) ├── определить платформу (Linux / macOS / Windows Git Bash)
│ uname: MINGW64_NT-... → windows, Linux → linux, Darwin → macos ├── проверить/установить just >= 1.36.0
├── проверить/установить uv >= 0.4.0
├── проверить/установить just (semver без sort -V — работает в Git Bash)
< 1.36.0 или отсутствует:
│ Linux/macOS → curl | bash → ~/.local/bin/just
│ macOS → brew install just (если есть Homebrew)
│ Windows → winget install --id Casey.Just
│ (перезапустить Git Bash после установки)
├── проверить/установить uv
< 0.4.0 или отсутствует:
│ Linux/macOS → curl -LsSf https://astral.sh/uv/install.sh | sh → ~/.local/bin/uv
│ macOS → brew install uv (если есть Homebrew)
│ Windows → powershell.exe irm https://astral.sh/uv/install.ps1 | iex
└── exec just host::bootstrap └── exec just host::bootstrap
├── [1/3] check-deps — just · uv · docker
├── [1/3] check-deps ├── [2/3] setup-udev — udev-правила NXP USB (только Linux)
│ just >= 1.36.0 · uv >= 0.4.0 · docker >= 24.0.0 │ 1FC9:0130 ← BootROM SDP
│ платформо-зависимые подсказки при ошибках │ 15A2:0073 ← Flashloader
└── [3/3] setup-tools — uv sync в tools/host/
├── [2/3] setup-udev (только Linux) SHA-256 uv.lock кешируется → повторный вызов мгновенный
│ /etc/udev/rules.d/99-nxp-mimxrt.rules:
│ 1FC9:0130 ← NXP BootROM (SDP-режим)
│ 15A2:0073 ← NXP Flashloader
│ VID:PID берутся из .env (BOOTROM_VID/PID, FLASHLOADER_VID/PID)
│ usermod -a -G plugdev $USER
│ требует re-login · на macOS/Windows пропускается
└── [3/3] setup-tools
uv sync в tools/host/
venv: .venv-host (Linux/macOS) · .venv-host-win (Windows)
разделение нужно: devcontainer создаёт .venv с Linux-симлинками,
Windows не может их удалить без прав администратора
SHA-256 uv.lock кэшируется в .cache/
повторный вызов мгновенный если lockfile не изменился
``` ```
### 5.4 После bootstrap ### 5.4 Настройка HIL-окружения (один раз, на хосте)
```bash ```bash
Открыть VSCode → "Reopen in Container" cd tools/hil
uv sync # установить pyocd, pyserial, pytest
# Прописать VCOM-порт в .env
# macOS: ls /dev/tty.usbmodem*
# Linux: ls /dev/ttyACM*
# Добавить в .env: HIL_VCOM_PORT=/dev/tty.usbmodemXXXX
``` ```
`postCreateCommand` выполняется автоматически при поднятии контейнера: ### 5.5 После bootstrap
```bash ```bash
cd tools/host && uv sync && # venv: tools/host/.venv (Linux, внутри контейнера) # Открыть VSCode → "Reopen in Container"
cd ../.. && # postCreateCommand выполняется автоматически:
cmake --preset host-debug && # uv sync (tools/host) + cmake --preset host-debug + cmake --preset Debug
cmake --preset Debug
``` ```
Прогрев CMake-кэша нужен чтобы clangd и IntelliSense заработали сразу,
без первой ручной сборки.
--- ---
## 6. Прошивки, boot-стратегии и матрица сборки ## 6. Прошивки, boot-стратегии и матрица сборки
### 6.1 Три подпроекта ### 6.1 Четыре типа сборки
| Прошивка | Boot-стратегия | DCD | Назначение | | Пресет | Toolchain | Назначение | Линкер-скрипт |
| --------------- | -------------------- | ---- | ---------------------------------------- | |--------|-----------|------------|---------------|
| `firmware_test` | XIP из Flash | ✅ | Входной контроль, тестирование периферии | | `Debug` / `Release` | ARM GCC | firmware_test, bootloader, tft_app | `flexspi_nor.ld` |
| `bootloader` | Копирование в ITCM | ❌ | Загрузчик, не использует SDRAM | | `host-debug` / `host-release` | clang (host) | Unity + fff тесты | — |
| `tft_app` | XIP + буферы в SDRAM | ✅ | Основное приложение (FreeRTOS, LCDIF) | | `target-debug` | ARM GCC | HIL target-прошивки | `ram.ld` |
### 6.2 Матрица сборки ### 6.2 CMake пресеты
Каждый проект собирается в двух режимах:
| | Debug | Release |
| --------------- | ------------------- | ------------------ |
| `firmware_test` | разработка, отладка | HAB для сервера |
| `bootloader` | отладка загрузчика | финальная прошивка |
| `tft_app` | отладка приложения | финальная прошивка |
### 6.3 CMake пресеты
```bash ```bash
configurePresets: Debug · Release · host-debug · host-release configurePresets:
Debug · Release ← firmware (XIP из Flash)
host-debug · host-release ← host unit-тесты
target-debug ← HIL target-прошивки (RAM)
buildPresets (ARM): buildPresets (ARM firmware):
all-debug / all-release ← все проекты (для CI)
firmware-test-debug / release firmware-test-debug / release
bootloader-debug / release bootloader-debug / release
app-debug / release app-debug / release
all-debug / all-release
buildPresets (host): buildPresets (host-тесты):
host-debug-build / host-release-build host-debug-build / host-release-build
buildPresets (HIL):
target-debug-build ← test_host_uart (и будущие HIL-прошивки)
``` ```
### 6.4 Карта Flash (W25Q128, 16 MB) ### 6.3 Boot-стратегии
```bash | Прошивка | Стратегия | DCD | Инструмент загрузки |
0x60000000 FCB — Flash Config Block 512 байт (пишет Flashloader) |----------|-----------|-----|---------------------|
0x60001000 IVT + BDT ← начало HAB-образа | `firmware_test` | XIP из Flash | ✅ | SPSDK → Flash |
0x60001040 DCD — инициализация SDRAM ~1088 байт (firmware_test, tft_app) | `bootloader` | Копирование в ITCM | ❌ | SPSDK → Flash |
0x60003000 Код прошивки (.text, .data…) | `tft_app` | XIP + буферы в SDRAM | ✅ | SPSDK → Flash |
``` | HIL target (`tests/target/`) | Исполнение из ITCM/DTCM | ❌ | pyOCD → RAM |
**HIL boot-стратегия (`bsp_boot_ram`):** pyOCD настраивает FLEXRAM (128KB ITCM + 128KB DTCM), записывает сегменты ELF по физическим адресам, устанавливает SP/PC из таблицы векторов и запускает выполнение. Flash не используется.
--- ---
@ -303,141 +268,171 @@ buildPresets (host):
### 7.1 Карта задач по контекстам ### 7.1 Карта задач по контекстам
| Задача | Где | | Задача | Где |
| -------------------------------------- | -------------------------- | |--------|-----|
| Написание кода, clangd, форматирование | devcontainer | | Написание кода, clangd, форматирование | devcontainer |
| Статический анализ (clang-tidy) | devcontainer | | Статический анализ (clang-tidy) | devcontainer |
| Host-тесты (Unity + fff) | devcontainer | | Host unit-тесты (Unity + fff) | devcontainer |
| Сборка ARM firmware (ELF) | devcontainer | | Сборка ARM firmware (ELF) | devcontainer |
| Подготовка HAB-образов (nxpimage) | devcontainer | | Сборка HIL target-прошивок | devcontainer |
| Прошивка платы через USB | **хост** | | Подготовка HAB-образов (nxpimage) | devcontainer |
| Отладка — GDB-сервер (JLink/probe-rs) | **хост** | | Прошивка платы через USB ROM | **хост** |
| Отладка — GDB-клиент | devcontainer → хост по TCP | | HIL-тесты (pyOCD + pytest) | **хост** |
| Target-тесты (управление стендом) | **хост** | | Отладка — GDB-сервер (JLink) | **хост** |
| Отладка — GDB-клиент | devcontainer → хост по TCP |
### 7.2 Типичная сессия разработки ### 7.2 Типичная сессия разработки
```bash ```bash
Открыть VSCode → работать в devcontainer весь день # Devcontainer (терминал VSCode)
just build::test-host # host unit-тесты — зелёные?
├── писать код just build::build-firmware-test-debug # ELF собирается?
just build::hab-firmware-test-debug # HAB-образ готов
├── Ctrl+Shift+P → "Run Task" → 🧪 Host Tests (Debug)
│ или в терминале: just build::test-host # Хостовый терминал — прошивка
just flash # прошить firmware_test debug во Flash
├── Ctrl+Shift+P → "Run Task" → 🔨 Build → firmware-test · debug
│ → build/Debug/firmware/test/firmware_test.elf # Хостовый терминал — HIL
just build::build-hil # собрать HIL target-прошивку
├── Ctrl+Shift+P → "Run Task" → 📦 HAB Image → firmware-test · debug # (можно делать в devcontainer)
│ → build/Debug/firmware_test_hab.bin just host::hil-run # загрузить ELF → запустить pytest
│ Переключиться в хостовый терминал # Отладка
# F5 в VSCode → запустить JLinkGDBServer на хосте → attach через cortex-debug
├── just flash firmware_test debug ← прошить отладочный образ
│ или
├── just host::flash-ram firmware_test debug ← загрузить в RAM (быстро, без износа Flash)
└── F5 в VSCode → отладка через JLink
``` ```
### 7.3 VSCode Tasks (внутри devcontainer) ### 7.3 VSCode Tasks (внутри devcontainer)
| Таск | Input 1 | Input 2 | Команда | | Таск | Команда |
| ---------------------- | ------- | ------------- | ------------------------------------ | |------|---------|
| 🔨 Build | project | debug/release | `just build::build-<project>-<type>` | | 🔨 Build | `just build::build-<project>-<type>` |
| 🧪 Host Tests (Debug) | — | — | `just build::test-host` | | 🧪 Host Tests (Debug) | `just build::test-host` |
| 🧪 Host Tests (Release) | — | — | `just build::test-host-release` | | 🧪 Host Tests (Release) | `just build::test-host-release` |
| 📦 HAB Image | project | debug/release | `just build::hab-<project>-<type>` | | 🎯 Build HIL Target Tests | `just build::build-hil` |
| 📦 HAB All (Debug) | — | — | `just build::hab-all-debug` | | 📦 HAB Image | `just build::hab-<project>-<type>` |
| 📦 HAB All (Release) | — | — | `just build::hab-all-release` | | 📦 HAB All (Debug/Release) | `just build::hab-all-debug/release` |
| 🗑️ Clean | — | — | `just build::clean` | | 🗑️ Clean | `just build::clean` |
Таски «Build» и «HAB Image» запрашивают два input последовательно:
сначала проект (`firmware-test / bootloader / app / all`),
затем тип (`debug / release`).
--- ---
## 8. Прошивка платы (хост) ## 8. Прошивка платы (хост)
### 8.1 Перевод платы в SDP-режим ### Перевод в SDP-режим
```bash ```bash
1. BOOT_MOD_1 → 3V3 1. BOOT_MOD_1 → 3V3
2. Reset 2. Reset
3. Подключить USB к хосту 3. Подключить USB → плата как VID:PID 1FC9:0130
→ плата определяется как VID:PID = 1FC9:0130 4. just host::flash <project> <type>
4. Выполнить нужный just host::flash-* рецепт 5. После прошивки: BOOT_MOD_1 → GND → Reset
5. После прошивки: BOOT_MOD_1 → GND, Reset
→ плата стартует из Flash
``` ```
### 8.2 Команды прошивки ### Команды прошивки
```bash ```bash
# Основной рецепт (project × type) # project × type
just host::flash firmware_test debug # разработка — итерации с отладчиком just host::flash firmware_test debug
just host::flash firmware_test release # проверить как будет на сервере just host::flash firmware_test release
just host::flash bootloader debug
just host::flash bootloader release just host::flash bootloader release
just host::flash tft_app debug
just host::flash tft_app release just host::flash tft_app release
# Загрузка в RAM — без записи во Flash, мгновенный старт # В RAM — быстро, без износа Flash
# Удобно для частых итераций: не изнашивает Flash just host::flash-ram firmware_test
just host::flash-ram firmware_test # default: debug
just host::flash-ram firmware_test debug
just host::flash-ram tft_app release
# Быстрые псевдонимы (из корневого Justfile) # Псевдонимы
just flash # = just host::flash-test-debug just flash # = firmware_test debug → Flash
just host::flash-test-debug just host::flash-production # bootloader + tft_app release
just host::flash-test-release
just host::flash-production # bootloader release + tft_app release
```
### 8.3 Что происходит внутри flash_usb.py
`flash_usb.py` читает конфигурацию из окружения (just экспортирует `.env`):
- `BOOTROM_VID/PID`, `FLASHLOADER_VID/PID` → USB-адреса устройств
- `BUILD_DIR` → абсолютный путь к артефактам сборки
```bash
Плата в SDP-режиме (1FC9:0130)
├── sdphost: загрузить ivt_flashloader.bin в RAM (0x20001C00)
└── sdphost: jump-address 0x20001C00
│ ожидание до 10с — Flashloader поднимается как 15A2:0073
Flashloader (15A2:0073)
├── fill-memory + configure-memory (0xC0000007) — инициализация FlexSPI
├── flash-erase-region 0x60000000
├── fill-memory + configure-memory (0xF000000F) — запись FCB
├── write-memory 0x60001000 ← HAB-образ
└── reset
``` ```
--- ---
## 9. Отладка ## 9. HIL-тесты (хост)
JLink/probe-rs работает на хосте напрямую через USB/SWD. HIL-тесты проверяют периферию на реальном железе. MCU-Link обеспечивает два канала по одному USB: SWD (прошивка через pyOCD) и VCOM (UART CLI).
GDB-клиент в devcontainer подключается к серверу по TCP.
```bash
pytest → uart_cmd("PING\r\n")
↓ pyserial / VCOM
MCU-Link
↓ LPUART1
RT1052 (HIL прошивка)
→ "PONG\r\n"
```
### Команды
```bash
# Сборка HIL target-прошивок (devcontainer)
just build::build-hil
# Запуск тестов (хост)
just host::hil-run # загрузить ELF + все тесты
just host::hil-smoke # только smoke-тесты (-m smoke)
just host::hil-run-fast # тесты без перезагрузки ELF (--no-load)
just host::hil-load # только загрузить ELF (без pytest)
```
### Архитектура HIL-теста
Каждый HIL-тест — пара: C-прошивка в `tests/target/<n>/` и pytest-файл в `tools/hil/test_<n>.py`. Прошивка реализует текстовый CLI через `bsp_uart_host`. pytest управляет через `uart_cmd()`.
Протокол готовности: прошивка шлёт `READY\r\n` в цикле пока хост не откроет порт — исключает race condition между загрузкой ELF и открытием COM-порта.
Гайд по добавлению нового HIL-теста — [tests/HIL_CREATE_TEST.md](../tests/HIL_CREATE_TEST.md).
---
## 10. Тестирование
### 10.1 Host unit-тесты
```bash
Фреймворк: Unity + fff
Пресеты: host-debug / host-release
Компилятор: clang-17 (не ARM GCC)
Запуск: just build::test-host
```
BSP-модули тестируются через fff-фейки и stub-хедеры в `tests/host/mocks/`. `BUILD_TESTS_HOST=ON` отключает ARM-специфику.
Гайд — [tests/HOST_CREATE_TEST.md](../tests/HOST_CREATE_TEST.md).
### 10.2 HIL target-тесты
```bash
Инструменты: pyOCD (SWD) + pyserial (UART) + pytest
Пресет: target-debug → ram.ld → ITCM/DTCM
Запуск: just host::hil-run
```
Текущие тесты: `test_uart.py` — PING/ECHO/BUF_SIZE через `bsp_uart_host`.
### 10.3 Будущие HIL-тесты (с M5StampPLC)
Для тестов с внешними интерфейсами (CAN, GPIO, RS485) понадобится промежуточное звено:
```bash
pytest → M5StampPLC (Arduino CLI) → CAN/GPIO → RT1052
```
M5StampPLC реализует тот же текстовый CLI — pytest работает одинаково с обоими каналами.
---
## 11. Отладка
```bash ```bash
Хост Хост
├── JLinkGDBServer -device MIMXRT1052 -if SWD -port 2331 ├── JLinkGDBServer -device MIMXRT1052 -if SWD -port 2331
│ USB/SWD → плата │ USB/SWD → плата
└── host.docker.internal:2331 ← доступен из devcontainer └── host.docker.internal:2331 ← доступен из devcontainer
Devcontainer Devcontainer
└── arm-none-eabi-gdb / probe-rs └── arm-none-eabi-gdb
target remote host.docker.internal:2331 target remote host.docker.internal:2331
``` ```
`.vscode/launch.json`: `.vscode/launch.json` (cortex-debug, тип `external`):
```json ```json
{ {
@ -448,78 +443,29 @@ Devcontainer
} }
``` ```
`host.docker.internal` — стандартный DNS-алиас Docker для хоста.
Работает на macOS, Windows и Linux (Docker Desktop).
> USB-passthrough программатора в Docker на macOS невозможен, на других
> платформах нестабилен. TCP-мост — единственное надёжное решение для
> всех платформ.
--- ---
## 10. Тесты ## 12. Жизненный цикл изменений
### 10.1 Host-тесты
Выполняются в devcontainer на хостовом компиляторе (x86/arm64). Железо не нужно.
```bash
Фреймворк: Unity + fff
Пресеты: host-debug / host-release
Компилятор: системный clang-17 (не ARM GCC)
Запуск: just build::test-host
Результат: JUnit XML → VSCode CTest Lab + GitLab CI
```
`BUILD_TESTS_HOST=ON` отключает ARM-специфику (BSP, SDK) — компилируются
только тестируемые модули и fff-заглушки.
### 10.2 Target-тесты
Выполняются на физической плате. Прошивается `firmware_test`,
стенд подаёт сигналы и проверяет ответы.
```bash
Стенд (M5StampPLC или аналог) ←→ Плата TFT
Тестируемые подсистемы:
SDRAM 32 MB — чтение/запись паттернов
QSPI Flash — erase / write / verify
CAN — loopback + внешний фрейм от стенда
UART TTL — echo-тест
UART +24V изол. — echo-тест
Гальв. входы +24V — состояния при подаче напряжения от стенда
RTC BM8563 — установка / чтение времени
SD-карта (SDIO) — монтирование, R/W файл
MQS (аудио) — воспроизведение тестового сигнала
IR-приёмник — приём тестового кода от стенда
```
---
## 11. Жизненный цикл изменений
```bash ```bash
feature-ветка feature-ветка
├── код в devcontainer ├── devcontainer
│ just build::test-host ← зелёные? │ just build::test-host ← зелёные host-тесты?
│ just build::build-firmware-test-debug ← компилируется? │ just build::build-firmware-test-debug
│ just build::build-hil ← HIL-прошивки собираются?
├── проверка на железе ├── хост
│ just build::hab-firmware-test-debug │ just flash ← прошить, проверить на железе
│ just flash ← прошить (алиас flash-test-debug) │ just host::hil-run ← HIL зелёные?
│ target-тесты со стендом ← периферия работает?
├── подготовка к MR ├── подготовка к MR
│ just build::hab-all-release ← финальные образы │ just build::hab-all-release
│ just host::flash firmware_test release ← убедиться что release работает │ just host::flash firmware_test release
└── Merge Request → GitLab └── Merge Request → GitLab CI
[CI pipeline — отдельная тема] host-тесты · сборка · HIL · публикация артефактов
host-тесты, сборка, публикация артефактов
Производственный сервер Производственный сервер
just host::incoming → firmware_test release → HIL just host::incoming → firmware_test release → HIL
just host::production → bootloader + tft_app release just host::production → bootloader + tft_app release
@ -527,105 +473,72 @@ feature-ветка
--- ---
## 12. Обновление зависимостей ## 13. Производственный сервер
### spsdk Сервер работает только с готовыми проверенными артефактами. Никакой сборки.
```bash ```bash
just host::upgrade-tools 3.8.0 Сценарий входного контроля:
git add tools/host/uv.lock tools/host/pyproject.toml just host::incoming
git commit -m "chore: upgrade spsdk to 3.8.0" └── flash firmware_test release → HIL-тесты
```
После этого у всех разработчиков и в контейнере обновится автоматически Финальная прошивка:
при следующем `just host::setup-tools` / `uv sync`. just host::production
├── flash bootloader release
└── flash tft_app release
### just в Dockerfile Установлено: just · uv + spsdk · uv + pyocd/pytest · git
```dockerfile
ARG JUST_VERSION=1.36.0 # .devcontainer/Dockerfile — единственное место
```
### ARM toolchain
```dockerfile
ARG TOOLCHAIN_VERSION=14.2.rel1 # .devcontainer/Dockerfile
```
Пересборка инвалидирует только Stage 1 (toolchain). Stage 2
(clang, cmake и др.) берётся из кэша — пересборка быстрая.
### После git pull если изменился uv.lock
```bash
just host::setup-tools # автоматически обнаружит изменение и выполнит uv sync
```
---
## 13. Производственный сервер (к сведению)
Сервер работает **только с готовыми проверенными артефактами**.
Никакой сборки, никакого компилятора.
```bash
Источник: GitLab Releases или FTP
— только теггированные релизы, прошедшие CI
Сценарий входного контроля новой платы:
1. just host::incoming
└── flash firmware_test release → HIL-тесты периферии
2. Тесты пройдены:
just host::production
├── flash bootloader release
└── flash tft_app release
Установлено: just · uv + spsdk · git
НЕ установлено: docker · cmake · компилятор · ARM toolchain НЕ установлено: docker · cmake · компилятор · ARM toolchain
``` ```
Детальный `server.just` и пайплайн сервера — отдельная задача.
--- ---
## Приложение А: минимальные версии ## Приложение А: минимальные версии
| Инструмент | Версия | Причина | | Инструмент | Версия | Причина |
| ------------ | --------- | ------------------------------------------------------------ | |------------|--------|---------|
| `just` | 1.36.0 | поддержка `mod` с кастомным путём (`mod host 'just/host.just'`) | | `just` | 1.36.0 | поддержка `mod` с кастомным путём |
| `uv` | 0.4.0 | стабильный lockfile формат | | `uv` | 0.4.0 | стабильный lockfile формат |
| `docker` | 24.0.0 | Compose v2, `--build-arg` | | `docker` | 24.0.0 | Compose v2 |
| `spsdk` | 3.7.x | совместимость с HAB yaml-форматом | | `spsdk` | 3.7.x | совместимость с HAB yaml-форматом |
| ARM GCC | 13.3.rel1 | C11, LTO, текущий SDK | | `pyocd` | 0.36+ | cortex_m target, write_core_register API |
| clang/clangd | 17 | поддержка `If:` в `.clangd` | | ARM GCC | 13.3.rel1 | C11, текущий SDK |
| clang/clangd | 17 | поддержка `If:` в `.clangd` |
## Приложение Б: быстрая шпаргалка ---
## Приложение Б: шпаргалка
```bash ```bash
# ── Первый запуск ────────────────────────────────────────────── # ── Первый запуск ───────────────────────────────────────────
./bootstrap.sh # инициализация хоста ./bootstrap.sh
cd tools/hil && uv sync
# VSCode → Reopen in Container # VSCode → Reopen in Container
# ── Внутри devcontainer (терминал VSCode) ────────────────────── # ── devcontainer ────────────────────────────────────────────
just build::test-host just build::test-host # host unit-тесты
just build::build-firmware-test-debug just build::build-firmware-test-debug # сборка firmware
just build::build-tft-app-release just build::build-hil # сборка HIL target-прошивок
just build::hab-firmware-test-debug just build::hab-firmware-test-debug # HAB-образ Debug
just build::hab-all-release just build::hab-all-release # все HAB Release
just build::clean just build::clean # очистить build/
# ── Хостовый терминал — прошивка ─────────────────────────────── # ── хост — прошивка ─────────────────────────────────────────
just flash # алиас: firmware_test debug just flash # firmware_test debug → Flash
just host::flash firmware_test debug # во Flash
just host::flash firmware_test release just host::flash firmware_test release
just host::flash bootloader release just host::flash bootloader release
just host::flash tft_app release just host::flash-production # bootloader + tft_app release
just host::flash-ram firmware_test # в RAM (быстро, без износа Flash) just host::flash-ram firmware_test # в RAM (без износа Flash)
just host::flash-production # bootloader + tft_app release
# ── Хостовый терминал — обслуживание ─────────────────────────── # ── хост — HIL-тесты ────────────────────────────────────────
just host::scan # найти NXP USB-устройства just host::hil-run # загрузить ELF + все тесты
just host::sdp-status # проверить BootROM just host::hil-smoke # только smoke
just host::setup-tools # обновить spsdk после git pull just host::hil-run-fast # без перезагрузки ELF
just host::check-deps # проверить версии инструментов just host::hil-load # только загрузить ELF
# ── хост — обслуживание ─────────────────────────────────────
just host::check-deps # проверить версии
just host::setup-tools # uv sync после git pull
just host::scan # найти NXP USB-устройства
just host::upgrade-tools 3.8.0 # обновить spsdk
``` ```

View file

@ -1,15 +1,12 @@
# firmware/test/CMakeLists.txt Тестовая прошивка — входной контроль платы на # firmware/test/CMakeLists.txt Тестовая прошивка — входной контроль платы на
# производстве # производстве
add_executable(firmware_test) set(TARGET_NAME firmware_test)
set(BSP_GENERATED ${CMAKE_SOURCE_DIR}/bsp/generated)
target_sources(
firmware_test
PRIVATE main.c ${PROJECT_SOURCE_DIR}/bsp/generated/syscalls.c
${BSP_GENERATED}/clock_config.c
${BSP_GENERATED}/startup/startup_MIMXRT1052.S)
target_compile_definitions(firmware_test add_executable(${TARGET_NAME} main.c ${BSP_GENERATED}/clock_config.c
${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE})
target_compile_definitions(${TARGET_NAME}
PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512) PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512)
# #
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@ -17,8 +14,8 @@ target_compile_definitions(firmware_test
# даёт: sdk_device, sdk_clock, sdk_common, CPU_MIMXRT1052CVJ5B, XIP_* дефайны # даёт: sdk_device, sdk_clock, sdk_common, CPU_MIMXRT1052CVJ5B, XIP_* дефайны
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
target_link_libraries( target_link_libraries(
firmware_test ${TARGET_NAME}
PRIVATE bsp_board bsp_led bsp_tick bsp_uart_host bsp_boot_ram PRIVATE bsp_board bsp_led bsp_tick bsp_uart_host bsp_boot_xip
lib_external # SEGGER RTT если включён через SEGGER_RTT_ENABLED lib_external # SEGGER RTT если включён через SEGGER_RTT_ENABLED
) )
@ -27,14 +24,16 @@ target_link_libraries(
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# --gc-sections — удалять неиспользуемые секции (работает с # --gc-sections — удалять неиспользуемые секции (работает с
# -ffunction/data-sections) --print-memory-usage — выводить таблицу # -ffunction/data-sections) --print-memory-usage — выводить таблицу
# использования Flash/RAM после линковки -Map — генерировать map-файл для # использования Flash/RAM после линковки -Map — генерировать
# анализа размещения символов -T — линкерный скрипт с описанием карты памяти # map-файл для анализа размещения символов -T — линкерный
# IMXRT1052 # скрипт с описанием карты памяти IMXRT1052
target_link_options( target_link_options(
firmware_test PRIVATE -Wl,--gc-sections -Wl,--print-memory-usage ${TARGET_NAME} PRIVATE -Wl,--gc-sections -Wl,--print-memory-usage
-Wl,-Map=${CMAKE_BINARY_DIR}/firmware_test.map -Wl,-Map=${CMAKE_BINARY_DIR}/firmware_test.map
-T${PROJECT_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_ram.ld) -T${PROJECT_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_flexspi_nor.ld)
set_target_properties(${TARGET_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR})
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Post-build: генерация .bin для прошивки через blhost # Post-build: генерация .bin для прошивки через blhost
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@ -43,7 +42,5 @@ add_custom_command(
POST_BUILD POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:firmware_test> COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:firmware_test>
${CMAKE_BINARY_DIR}/firmware_test.bin ${CMAKE_BINARY_DIR}/firmware_test.bin
COMMAND ${CMAKE_OBJCOPY} -O ihex $<TARGET_FILE:firmware_test>
${CMAKE_BINARY_DIR}/firmware_test.hex
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:firmware_test> COMMAND ${CMAKE_SIZE} $<TARGET_FILE:firmware_test>
COMMENT "Generating firmware_test.bin") COMMENT "Generating firmware_test.bin")

View file

@ -4,8 +4,8 @@
# #
# Рабочая директория — корень репозитория (не scripts/). # Рабочая директория — корень репозитория (не scripts/).
# ============================================================================= # =============================================================================
# === Импорт общих переменных === # === Импорт общих переменных ===
TOOLS_DIR := env('TOOLS_DIR', justfile_directory() / 'tools/host') TOOLS_DIR := env('TOOLS_DIR', justfile_directory() / 'tools/host')
BUILD_DIR := env('BUILD_DIR', justfile_directory() / 'build') BUILD_DIR := env('BUILD_DIR', justfile_directory() / 'build')
@ -16,14 +16,8 @@ _default:
# ============================================================================= # =============================================================================
# ГРУППА: build # ГРУППА: build
#
# Матрица сборки:
# firmware-test × debug / release
# bootloader × debug / release
# app × debug / release
# all × debug / release (все проекты сразу, для CI)
# ============================================================================= # =============================================================================
# Приватные рецепты для configure (не показываются в --list)
[private] [private]
_configure-debug: _configure-debug:
cmake --preset Debug cmake --preset Debug
@ -32,49 +26,68 @@ _configure-debug:
_configure-release: _configure-release:
cmake --preset Release cmake --preset Release
[private]
_configure-target-debug:
cmake --preset target-debug
# ── firmware_test ───────────────────────────────────────────────────────────── # ── firmware_test ─────────────────────────────────────────────────────────────
[doc('Собрать firmware_test (Debug)')]
[group('build')] [group('build')]
build-firmware-test-debug: _configure-debug build-firmware-test-debug: _configure-debug
cmake --build --preset firmware-test-debug cmake --build --preset firmware-test-debug
[doc('Собрать firmware_test (Release)')]
[group('build')] [group('build')]
build-firmware-test-release: _configure-release build-firmware-test-release: _configure-release
cmake --build --preset firmware-test-release cmake --build --preset firmware-test-release
# ── bootloader ─────────────────────────────────────────────────────────────── # ── bootloader ───────────────────────────────────────────────────────────────
[doc('Собрать bootloader (Debug)')]
[group('build')] [group('build')]
build-bootloader-debug: _configure-debug build-bootloader-debug: _configure-debug
cmake --build --preset bootloader-debug cmake --build --preset bootloader-debug
[doc('Собрать bootloader (Release)')]
[group('build')] [group('build')]
build-bootloader-release: _configure-release build-bootloader-release: _configure-release
cmake --build --preset bootloader-release cmake --build --preset bootloader-release
# ── app ─────────────────────────────────────────────────────────────────────── # ── app ───────────────────────────────────────────────────────────────────────
[doc('Собрать tft_app (Debug)')]
[group('build')] [group('build')]
build-app-debug: _configure-debug build-app-debug: _configure-debug
cmake --build --preset app-debug cmake --build --preset app-debug
[doc('Собрать tft_app (Release)')]
[group('build')] [group('build')]
build-app-release: _configure-release build-app-release: _configure-release
cmake --build --preset app-release cmake --build --preset app-release
# ── все проекты сразу ───────────────────────────────────────────────────────── # ── все проекты сразу ─────────────────────────────────────────────────────────
[doc('Собрать все проекты (Debug)')]
[group('build')] [group('build')]
build-all-debug: _configure-debug build-all-debug: _configure-debug
cmake --build --preset all-debug cmake --build --preset all-debug
[doc('Собрать все проекты (Release)')]
[group('build')] [group('build')]
build-all-release: _configure-release build-all-release: _configure-release
cmake --build --preset all-release cmake --build --preset all-release
# ── утилиты ─────────────────────────────────────────────────────────────────── # ── утилиты ───────────────────────────────────────────────────────────────────
[group('build'), confirm("Delete build/ directory?")]
[confirm("Delete build/ directory?")]
[doc('Удалить директорию build/')]
[group('build')]
clean: clean:
rm -rf build rm -rf build
# ============================================================================= # =============================================================================
# ГРУППА: test # ГРУППА: test — host-тесты (Unity + fff, в devcontainer)
# ============================================================================= # =============================================================================
[private] [private]
@ -85,145 +98,154 @@ _configure-host-debug:
_configure-host-release: _configure-host-release:
cmake --preset host-release cmake --preset host-release
[doc('Собрать и запустить host unit-тесты (Debug)')]
[group('test')] [group('test')]
test-host: _configure-host-debug test-host: _configure-host-debug
cmake --build --preset host-debug-build cmake --build --preset host-debug-build
ctest --preset host-debug-test ctest --preset host-debug-test
[doc('Собрать и запустить host unit-тесты (Release)')]
[group('test')] [group('test')]
test-host-release: _configure-host-release test-host-release: _configure-host-release
cmake --build --preset host-release-build cmake --build --preset host-release-build
ctest --preset host-release-test ctest --preset host-release-test
# ============================================================================= # =============================================================================
# ГРУППА: flash — подготовка HAB-образов # ГРУППА: hil — сборка target-прошивок для HIL-тестов
# # Запуск самих тестов — на хосте через just host::hil-*
# Матрица HAB:
# firmware-test × debug / release
# bootloader × debug / release
# app × debug / release
# all × debug / release
#
# Debug HAB — для прошивки своей платы и отладки.
# Release HAB — для производственного сервера.
#
# HAB yaml конфиги:
# tools/host/hab/hab_<project>_debug.yaml — путь к Debug ELF
# tools/host/hab/hab_<project>_release.yaml — путь к Release ELF
# ============================================================================= # =============================================================================
[doc('Собрать все HIL target-прошивки (Debug)')]
[group('hil')]
build-hil: _configure-target-debug
cmake --build --preset target-debug-build
# =============================================================================
# ГРУППА: hab_image_gen — подготовка HAB-образов
# =============================================================================
# ── firmware_test ───────────────────────────────────────────────────────────── # ── firmware_test ─────────────────────────────────────────────────────────────
[doc('Собрать HAB-образ firmware_test (Debug)')]
[group('hab_image_gen')] [group('hab_image_gen')]
hab-firmware-test-debug: build-firmware-test-debug hab-firmware-test-debug: build-firmware-test-debug
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
mkdir -p "{{BUILD_DIR}}/Debug" mkdir -p "{{ BUILD_DIR }}/Debug"
cd "{{TOOLS_DIR}}/hab" cd "{{ TOOLS_DIR }}/hab"
uv run nxpimage hab export --force \ uv run nxpimage hab export --force \
-c hab_firmware_test_debug.yaml \ -c hab_firmware_test_debug.yaml \
-o "{{BUILD_DIR}}/Debug/firmware_test_hab.bin" -o "{{ BUILD_DIR }}/Debug/firmware_test_hab.bin"
echo " ✅ firmware_test_hab.bin (Debug)" echo " ✅ firmware_test_hab.bin (Debug)"
[doc('Собрать HAB-образ firmware_test (Release)')]
[group('hab_image_gen')] [group('hab_image_gen')]
hab-firmware-test-release: build-firmware-test-release hab-firmware-test-release: build-firmware-test-release
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
mkdir -p "{{BUILD_DIR}}/Release" mkdir -p "{{ BUILD_DIR }}/Release"
cd "{{TOOLS_DIR}}/hab" cd "{{ TOOLS_DIR }}/hab"
uv run nxpimage hab export --force \ uv run nxpimage hab export --force \
-c hab_firmware_test_release.yaml \ -c hab_firmware_test_release.yaml \
-o "{{BUILD_DIR}}/Release/firmware_test_hab.bin" -o "{{ BUILD_DIR }}/Release/firmware_test_hab.bin"
echo " ✅ firmware_test_hab.bin (Release)" echo " ✅ firmware_test_hab.bin (Release)"
# ── bootloader ─────────────────────────────────────────────────────────────── # ── bootloader ───────────────────────────────────────────────────────────────
[doc('Собрать HAB-образ bootloader (Debug)')]
[group('hab_image_gen')] [group('hab_image_gen')]
hab-bootloader-debug: build-bootloader-debug hab-bootloader-debug: build-bootloader-debug
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
mkdir -p "{{BUILD_DIR}}/Debug" mkdir -p "{{ BUILD_DIR }}/Debug"
cd "{{TOOLS_DIR}}/hab" cd "{{ TOOLS_DIR }}/hab"
uv run nxpimage hab export --force \ uv run nxpimage hab export --force \
-c hab_bootloader_debug.yaml \ -c hab_bootloader_debug.yaml \
-o "{{BUILD_DIR}}/Debug/bootloader_hab.bin" -o "{{ BUILD_DIR }}/Debug/bootloader_hab.bin"
echo " ✅ bootloader_hab.bin (Debug)" echo " ✅ bootloader_hab.bin (Debug)"
[doc('Собрать HAB-образ bootloader (Release)')]
[group('hab_image_gen')] [group('hab_image_gen')]
hab-bootloader-release: build-bootloader-release hab-bootloader-release: build-bootloader-release
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
mkdir -p "{{BUILD_DIR}}/Release" mkdir -p "{{ BUILD_DIR }}/Release"
cd "{{TOOLS_DIR}}/hab" cd "{{ TOOLS_DIR }}/hab"
uv run nxpimage hab export --force \ uv run nxpimage hab export --force \
-c hab_bootloader_release.yaml \ -c hab_bootloader_release.yaml \
-o "{{BUILD_DIR}}/Release/bootloader_hab.bin" -o "{{ BUILD_DIR }}/Release/bootloader_hab.bin"
echo " ✅ bootloader_hab.bin (Release)" echo " ✅ bootloader_hab.bin (Release)"
# ── app ─────────────────────────────────────────────────────────────────────── # ── app ───────────────────────────────────────────────────────────────────────
[doc('Собрать HAB-образ tft_app (Debug)')]
[group('hab_image_gen')] [group('hab_image_gen')]
hab-app-debug: build-app-debug hab-app-debug: build-app-debug
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
mkdir -p "{{BUILD_DIR}}/Debug" mkdir -p "{{ BUILD_DIR }}/Debug"
cd "{{TOOLS_DIR}}/hab" cd "{{ TOOLS_DIR }}/hab"
uv run nxpimage hab export --force \ uv run nxpimage hab export --force \
-c hab_app_debug.yaml \ -c hab_app_debug.yaml \
-o "{{BUILD_DIR}}/Debug/app_hab.bin" -o "{{ BUILD_DIR }}/Debug/app_hab.bin"
echo " ✅ app_hab.bin (Debug)" echo " ✅ app_hab.bin (Debug)"
[doc('Собрать HAB-образ tft_app (Release)')]
[group('hab_image_gen')] [group('hab_image_gen')]
hab-app-release: build-app-release hab-app-release: build-app-release
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
mkdir -p "{{BUILD_DIR}}/Release" mkdir -p "{{ BUILD_DIR }}/Release"
cd "{{TOOLS_DIR}}/hab" cd "{{ TOOLS_DIR }}/hab"
uv run nxpimage hab export --force \ uv run nxpimage hab export --force \
-c hab_app_release.yaml \ -c hab_app_release.yaml \
-o "{{BUILD_DIR}}/Release/app_hab.bin" -o "{{ BUILD_DIR }}/Release/app_hab.bin"
echo " ✅ app_hab.bin (Release)" echo " ✅ app_hab.bin (Release)"
# ── все образы сразу ───────────────────────────────────────────────────────── # ── все образы сразу ─────────────────────────────────────────────────────────
[doc('Собрать HAB-образы всех проектов (Debug)')]
[group('hab_image_gen')] [group('hab_image_gen')]
hab-all-debug: hab-firmware-test-debug hab-bootloader-debug hab-app-debug hab-all-debug: hab-firmware-test-debug hab-bootloader-debug hab-app-debug
@echo " ✅ All HAB images (Debug) ready" @echo " ✅ All HAB images (Debug) ready"
[doc('Собрать HAB-образы всех проектов (Release)')]
[group('hab_image_gen')] [group('hab_image_gen')]
hab-all-release: hab-firmware-test-release hab-bootloader-release hab-app-release hab-all-release: hab-firmware-test-release hab-bootloader-release hab-app-release
@echo " ✅ All HAB images (Release) ready" @echo " ✅ All HAB images (Release) ready"
# ── верификация образа (для отладки) ───────────────────────────────────────── # ── верификация образа ────────────────────────────────────────────────────────
# Использование: just --justfile just/build.just hab-verify firmware_test debug
[doc('Верифицировать HAB-образ: hab-verify <project> <debug|release>')]
[group('hab_image_gen')] [group('hab_image_gen')]
hab-verify project="firmware_test" type="release": hab-verify project="firmware_test" type="release":
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
case "{{project}}-{{type}}" in case "{{ project }}-{{ type }}" in
firmware_test-debug) BIN="{{BUILD_DIR}}/Debug/firmware_test_hab.bin" ;; firmware_test-debug) BIN="{{ BUILD_DIR }}/Debug/firmware_test_hab.bin" ;;
firmware_test-release) BIN="{{BUILD_DIR}}/Release/firmware_test_hab.bin" ;; firmware_test-release) BIN="{{ BUILD_DIR }}/Release/firmware_test_hab.bin" ;;
bootloader-debug) BIN="{{BUILD_DIR}}/Debug/bootloader_hab.bin" ;; bootloader-debug) BIN="{{ BUILD_DIR }}/Debug/bootloader_hab.bin" ;;
bootloader-release) BIN="{{BUILD_DIR}}/Release/bootloader_hab.bin" ;; bootloader-release) BIN="{{ BUILD_DIR }}/Release/bootloader_hab.bin" ;;
app-debug) BIN="{{BUILD_DIR}}/Debug/app_hab.bin" ;; app-debug) BIN="{{ BUILD_DIR }}/Debug/app_hab.bin" ;;
app-release) BIN="{{BUILD_DIR}}/Release/app_hab.bin" ;; app-release) BIN="{{ BUILD_DIR }}/Release/app_hab.bin" ;;
*) *)
echo " ❌ Unknown: {{project}}-{{type}}" echo " ❌ Unknown: {{ project }}-{{ type }}"
echo " Projects: firmware_test, bootloader, app"
echo " Types: debug, release"
exit 1 ;; exit 1 ;;
esac esac
OUT="/tmp/hab_parse_{{project}}_{{type}}.yaml" OUT="/tmp/hab_parse_{{ project }}_{{ type }}.yaml"
cd "{{TOOLS_DIR}}" && uv run nxpimage hab parse -b "${BIN}" -o "${OUT}" cd "{{ TOOLS_DIR }}" && uv run nxpimage hab parse -b "${BIN}" -o "${OUT}"
echo " ✅ ${OUT}" echo " ✅ ${OUT}"
grep -E "(entry|csf|tag)" "${OUT}" || true grep -E "(entry|csf|tag)" "${OUT}" || true
# ============================================================================= # =============================================================================
# ГРУППА: quality — линтеры, форматтеры # ГРУППА: quality
# ============================================================================= # =============================================================================
# TODO: проверять bsp,firmware, modules
[doc('Запустить линтеры (clang-tidy)')]
[group('quality')] [group('quality')]
lint: lint:
echo "TODO!" echo "TODO!"
# clang-tidy -p {{BUILD_DIR}}/Debug src/**/*.c
[doc('Форматировать код (clang-format)')]
[group('quality')] [group('quality')]
format: format:
echo "TODO!" echo "TODO!"
# clang-format -i src/**/*.{c,h}

View file

@ -4,50 +4,53 @@
# #
# Предполагается что CI работает внутри devcontainer. # Предполагается что CI работает внутри devcontainer.
# ============================================================================= # =============================================================================
# === Импорт общих переменных === # === Импорт общих переменных ===
BUILD_DIR := env('BUILD_DIR', justfile_directory() / 'build') BUILD_DIR := env('BUILD_DIR', justfile_directory() / 'build')
set working-directory := '..' set working-directory := '..'
# ============================================================================= # =============================================================================
# ГРУППА: ci — полные пайплайны # ГРУППА: ci — полные пайплайны
# ============================================================================= # =============================================================================
# Полный CI pipeline: сборка + тесты + линтеры [doc('Полный CI pipeline: сборка + тесты + линтеры')]
[group('ci')] [group('ci')]
pipeline: build test lint pipeline: build test lint
@echo " ✅ CI pipeline complete" @echo " ✅ CI pipeline complete"
# Сборка всех проектов в Release [doc('Собрать все проекты в Release')]
[group('ci')] [group('ci')]
build: build:
just build::build-all-release just build::build-all-release
# Unit-тесты [doc('Запустить host unit-тесты (Release)')]
[group('ci')] [group('ci')]
test: test:
just build::test-host-release just build::test-host-release
# TODO: Линтеры + статический анализ [doc('Запустить линтеры и статический анализ')]
[group('ci')] [group('ci')]
lint: lint:
echo "TODO" echo "TODO"
#just build::lint #just build::lint
#just build::check #just build::check
# Генерация HAB-образов для релиза [doc('Собрать HAB Release-артефакты для выкладки')]
[group('ci')] [group('ci')]
release: build release: build
just build::hab-all-release just build::hab-all-release
@echo " ✅ Release artifacts ready in {{BUILD_DIR}}/Release/" @echo " ✅ Release artifacts ready in {{ BUILD_DIR }}/Release/"
# Генерация coverage отчёта (если gcov настроен) [group('ci')]
[group('ci'), private] [private]
_coverage: _coverage:
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
if command -v gcovr &>/dev/null; then if command -v gcovr &>/dev/null; then
echo " 📊 Generating coverage report..." echo " 📊 Generating coverage report..."
gcovr --xml -o {{BUILD_DIR}}/coverage.xml gcovr --xml -o {{ BUILD_DIR }}/coverage.xml
else else
echo " ⚠️ gcovr not found, skipping coverage" echo " ⚠️ gcovr not found, skipping coverage"
fi fi

View file

@ -1,33 +1,24 @@
# ============================================================================= # =============================================================================
# host.just — операции на хост-машине (вне devcontainer) # host.just — операции на хост-машине (вне devcontainer)
#
# Ответственность:
# - bootstrap окружения (just, uv, docker, udev)
# - USB-прошивка через spsdk
# - HIL-тесты на реальном железе
# - производственные сценарии (incoming, production)
#
# Все рецепты должны работать БЕЗ devcontainer.
# ============================================================================= # =============================================================================
# === Импорт общих переменных из корневого justfile ===
# Благодаря `set export` в корне, эти переменные доступны
TOOLS_DIR := env('TOOLS_DIR', justfile_directory() / 'tools/host') TOOLS_DIR := env('TOOLS_DIR', justfile_directory() / 'tools/host')
HIL_DIR := justfile_directory() / 'tools/hil'
CACHE_DIR := env('CACHE_DIR', justfile_directory() / '.cache') CACHE_DIR := env('CACHE_DIR', justfile_directory() / '.cache')
BUILD_DIR := env('BUILD_DIR', justfile_directory() / 'build')
_uname := `uname -s` _uname := `uname -s`
_venv := if _uname =~ "MINGW|MSYS|CYGWIN" { ".venv-host-win" } else { ".venv-host" } _venv := if _uname =~ "MINGW|MSYS|CYGWIN" { ".venv-host-win" } else { ".venv-host" }
JUST_MIN := "1.36.0"
# === Минимальные версии зависимостей === UV_MIN := "0.4.0"
JUST_MIN := "1.36.0"
UV_MIN := "0.4.0"
DOCKER_MIN := "24.0.0" DOCKER_MIN := "24.0.0"
set working-directory := '..' set working-directory := '..'
# ============================================================================= # =============================================================================
# ГРУППА: setup — инициализация рабочего окружения # ГРУППА: setup
# ============================================================================= # =============================================================================
# [1/3] Проверить наличие и версии инструментов на хост-машине [doc('Проверить версии и наличие зависимостей хоста (just, uv, docker)')]
[group('setup')] [group('setup')]
check-deps: check-deps:
#!/usr/bin/env bash #!/usr/bin/env bash
@ -48,7 +39,6 @@ check-deps:
*) PLATFORM="unknown" ;; *) PLATFORM="unknown" ;;
esac esac
# Сравнение semver без sort -V (недоступен в Git Bash)
semver_ge() { semver_ge() {
local a="$1" b="$2" local a="$1" b="$2"
local a1 a2 a3 b1 b2 b3 local a1 a2 a3 b1 b2 b3
@ -69,21 +59,21 @@ check-deps:
case "${PLATFORM}" in case "${PLATFORM}" in
linux) linux)
case "$tool" in case "$tool" in
just) echo "uv tool install 'rust-just=={{JUST_MIN}}'" ;; just) echo "uv tool install 'rust-just=={{ JUST_MIN }}'" ;;
uv) echo "curl -LsSf https://astral.sh/uv/install.sh | sh" ;; uv) echo "curl -LsSf https://astral.sh/uv/install.sh | sh" ;;
docker) echo "https://docs.docker.com/engine/install/ubuntu/" ;; docker) echo "https://docs.docker.com/engine/install/ubuntu/" ;;
esac ;; esac ;;
macos) macos)
case "$tool" in case "$tool" in
just) echo "uv tool install 'rust-just=={{JUST_MIN}}'" ;; just) echo "uv tool install 'rust-just=={{ JUST_MIN }}'" ;;
uv) echo "brew install uv" ;; uv) echo "brew install uv" ;;
docker) echo "brew install --cask docker" ;; docker) echo "brew install --cask docker" ;;
esac ;; esac ;;
windows) windows)
case "$tool" in case "$tool" in
just) echo "uv tool install 'rust-just=={{JUST_MIN}}'" ;; just) echo "uv tool install 'rust-just=={{ JUST_MIN }}'" ;;
uv) echo 'powershell -c "irm https://astral.sh/uv/install.ps1 | iex"' ;; uv) echo 'powershell -c "irm https://astral.sh/uv/install.ps1 | iex"' ;;
docker) echo "winget install --id Docker.DockerDesktop (в PowerShell)" ;; docker) echo "winget install --id Docker.DockerDesktop" ;;
esac ;; esac ;;
esac esac
} }
@ -115,9 +105,9 @@ check-deps:
echo -e "${BOLD} Checking host dependencies...${RESET}" echo -e "${BOLD} Checking host dependencies...${RESET}"
echo "" echo ""
check_tool "just" "just" "{{JUST_MIN}}" check_tool "just" "just" "{{ JUST_MIN }}"
check_tool "uv" "uv" "{{UV_MIN}}" check_tool "uv" "uv" "{{ UV_MIN }}"
check_tool "docker" "docker" "{{DOCKER_MIN}}" check_tool "docker" "docker" "{{ DOCKER_MIN }}"
if [[ "${PLATFORM}" == "linux" ]]; then if [[ "${PLATFORM}" == "linux" ]]; then
echo "" echo ""
@ -125,14 +115,14 @@ check-deps:
echo "" echo ""
rules_file="/etc/udev/rules.d/99-nxp-mimxrt.rules" rules_file="/etc/udev/rules.d/99-nxp-mimxrt.rules"
udev_ok=true udev_ok=true
grep -q "1fc9" "$rules_file" 2>/dev/null || { warn "udev: BootROM rule missing (VID 1fc9)"; udev_ok=false; } grep -q "1fc9" "$rules_file" 2>/dev/null || { warn "udev: BootROM rule missing"; udev_ok=false; }
grep -q "15a2" "$rules_file" 2>/dev/null || { warn "udev: Flashloader rule missing (VID 15a2)"; udev_ok=false; } grep -q "15a2" "$rules_file" 2>/dev/null || { warn "udev: Flashloader rule missing"; udev_ok=false; }
if ! groups 2>/dev/null | grep -q plugdev; then if ! groups 2>/dev/null | grep -q plugdev; then
warn "User '$(whoami)' not in 'plugdev' group" warn "User '$(whoami)' not in 'plugdev' group"
udev_ok=false udev_ok=false
fi fi
if [[ "$udev_ok" == "false" ]]; then if [[ "$udev_ok" == "false" ]]; then
warn "USB flashing requires root without udev rules -- run: just setup-udev" warn "Run: just host::setup-udev"
else else
ok "udev rules (NXP USB) + plugdev group" ok "udev rules (NXP USB) + plugdev group"
fi fi
@ -140,62 +130,54 @@ check-deps:
echo "" echo ""
if [[ $ERRORS -gt 0 ]]; then if [[ $ERRORS -gt 0 ]]; then
echo -e " ${RED}${BOLD}$ERRORS error(s) found. Fix them before continuing.${RESET}" echo -e " ${RED}${BOLD}$ERRORS error(s) found.${RESET}"
echo ""
exit 1 exit 1
elif [[ $WARNINGS -gt 0 ]]; then elif [[ $WARNINGS -gt 0 ]]; then
echo -e " ${YELLOW}${BOLD}$WARNINGS warning(s). Some features may not work.${RESET}" echo -e " ${YELLOW}${BOLD}$WARNINGS warning(s).${RESET}"
else else
echo -e " ${GREEN}${BOLD}All dependencies satisfied.${RESET}" echo -e " ${GREEN}${BOLD}All dependencies satisfied.${RESET}"
fi fi
echo "" echo ""
# [2/3] Установить udev правила для USB-прошивки (только Linux, требует sudo) [doc('Установить udev rules для NXP USB (только Linux)')]
[group('setup')] [group('setup')]
setup-udev: setup-udev:
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
case "$(uname -s)" in case "$(uname -s)" in
Linux*) ;; Linux*) ;;
*) *) echo " udev rules are Linux-only, skipping."; exit 0 ;;
echo " udev rules are Linux-only, skipping on $(uname -s)."
exit 0
;;
esac esac
echo " Installing udev rules for NXP USB devices..."
printf '%s\n%s\n%s\n%s\n' \ printf '%s\n%s\n%s\n%s\n' \
'# NXP BootROM -- SDP mode (BOOT_MOD_1 = 3V3)' \ '# NXP BootROM -- SDP mode' \
'SUBSYSTEM=="usb", ATTR{idVendor}=="1fc9", ATTR{idProduct}=="0130", MODE="0666", GROUP="plugdev"' \ 'SUBSYSTEM=="usb", ATTR{idVendor}=="1fc9", ATTR{idProduct}=="0130", MODE="0666", GROUP="plugdev"' \
'# NXP Flashloader -- after jump-address' \ '# NXP Flashloader' \
'SUBSYSTEM=="usb", ATTR{idVendor}=="15a2", ATTR{idProduct}=="0073", MODE="0666", GROUP="plugdev"' \ 'SUBSYSTEM=="usb", ATTR{idVendor}=="15a2", ATTR{idProduct}=="0073", MODE="0666", GROUP="plugdev"' \
| sudo tee /etc/udev/rules.d/99-nxp-mimxrt.rules > /dev/null | sudo tee /etc/udev/rules.d/99-nxp-mimxrt.rules > /dev/null
sudo udevadm control --reload-rules sudo udevadm control --reload-rules && sudo udevadm trigger
sudo udevadm trigger
sudo usermod -a -G plugdev "$USER" sudo usermod -a -G plugdev "$USER"
echo " ✅ udev rules installed" echo " ✅ udev rules installed. Re-login required."
echo " ⚠️ Re-login required for 'plugdev' group to take effect"
# [3/3] Синхронизировать зависимости tools/host (spsdk и др.) [doc('Установить/обновить Python-зависимости tools/host (spsdk и др.)')]
# Идемпотентен: пропускает sync если uv.lock не изменился
[group('setup')] [group('setup')]
setup-tools: setup-tools:
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
LOCK="{{TOOLS_DIR}}/uv.lock" LOCK="{{ TOOLS_DIR }}/uv.lock"
STAMP="{{CACHE_DIR}}/uv_lock.sha256" STAMP="{{ CACHE_DIR }}/uv_lock.sha256"
mkdir -p "{{CACHE_DIR}}" mkdir -p "{{ CACHE_DIR }}"
current=$(sha256sum "$LOCK" 2>/dev/null | cut -d' ' -f1 || echo "missing") current=$(sha256sum "$LOCK" 2>/dev/null | cut -d' ' -f1 || echo "missing")
previous=$(cat "$STAMP" 2>/dev/null || echo "none") previous=$(cat "$STAMP" 2>/dev/null || echo "none")
if [[ "$current" == "$previous" ]]; then if [[ "$current" == "$previous" ]]; then
echo " ✅ tools/host deps up to date (lockfile unchanged)" echo " ✅ tools/host deps up to date"
else else
echo " 📦 Syncing tools/host deps..." echo " 📦 Syncing tools/host deps..."
cd "{{TOOLS_DIR}}" && uv sync cd "{{ TOOLS_DIR }}" && uv sync
echo "$current" > "$STAMP" echo "$current" > "$STAMP"
echo " ✅ tools/host deps installed" echo " ✅ tools/host deps installed"
fi fi
# Полная инициализация — запустить один раз после git clone [doc('Полная первичная настройка хоста: check-deps + setup-udev + setup-tools')]
[group('setup')] [group('setup')]
bootstrap: bootstrap:
#!/usr/bin/env bash #!/usr/bin/env bash
@ -224,118 +206,143 @@ bootstrap:
echo "" echo ""
# ============================================================================= # =============================================================================
# ГРУППА: flash — прошивка платы через USB # ГРУППА: flash
# Требует: uv + spsdk (just setup-tools), плата в SDP-режиме
# HAB-образы должны быть собраны заранее (just hab-* внутри devcontainer)
#
# Матрица прошивки:
# just flash firmware_test debug
# just flash firmware_test release
# just flash bootloader debug
# just flash bootloader release
# just flash app debug
# just flash app release
# ============================================================================= # =============================================================================
# Прошить выбранный HAB-образ. [doc('Прошить образ во Flash через USB ROM: flash <firmware_test|bootloader|app> <debug|release>')]
# Использование: just flash <project> <type>
# project: firmware_test | bootloader | app
# type: debug | release (default: release)
#
# Образ должен быть собран заранее в devcontainer:
# just --justfile just/build.just hab-<project>-<type>
[group('flash')] [group('flash')]
flash project type="release": flash project type="release":
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
case "{{type}}" in case "{{ type }}" in
debug) BUILD_TYPE="Debug" ;; debug) BUILD_TYPE="Debug" ;;
release) BUILD_TYPE="Release" ;; release) BUILD_TYPE="Release" ;;
*) *) echo " ❌ Unknown type: {{ type }}"; exit 1 ;;
echo " ❌ Unknown type: {{type}}"
echo " Valid: debug, release"
exit 1 ;;
esac esac
cd "{{TOOLS_DIR}}" && UV_PROJECT_ENVIRONMENT={{_venv}} uv run python flash_usb.py \ cd "{{ TOOLS_DIR }}" && UV_PROJECT_ENVIRONMENT={{ _venv }} uv run python flash_usb.py \
--firmware "{{project}}" --build-type "${BUILD_TYPE}" --firmware "{{ project }}" --build-type "${BUILD_TYPE}"
# Загрузить образ в RAM без записи во Flash. [doc('Прошить образ в RAM через USB ROM: flash-ram <firmware_test|bootloader|app> <debug|release>')]
# Полезно для быстрой проверки — не изнашивает Flash, плата стартует сразу.
# Использование: just flash-ram <project> <type>
[group('flash')] [group('flash')]
flash-ram project type="debug": flash-ram project type="debug":
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
case "{{type}}" in case "{{ type }}" in
debug) BUILD_TYPE="Debug" ;; debug) BUILD_TYPE="Debug" ;;
release) BUILD_TYPE="Release" ;; release) BUILD_TYPE="Release" ;;
*) *) echo " ❌ Unknown type: {{ type }}"; exit 1 ;;
echo " ❌ Unknown type: {{type}}"
echo " Valid: debug, release"
exit 1 ;;
esac esac
cd "{{TOOLS_DIR}}" && UV_PROJECT_ENVIRONMENT={{_venv}} uv run python flash_usb.py \ cd "{{ TOOLS_DIR }}" && UV_PROJECT_ENVIRONMENT={{ _venv }} uv run python flash_usb.py \
--firmware "{{project}}" --build-type "${BUILD_TYPE}" --ram-only --firmware "{{ project }}" --build-type "${BUILD_TYPE}" --ram-only
# Псевдонимы для частых сценариев ──────────────────────────────────────────── [doc('Прошить firmware_test Debug во Flash')]
# firmware_test Debug — самый частый при разработке
[group('flash')] [group('flash')]
flash-test-debug: flash-test-debug:
@just host::flash firmware_test debug @just host::flash firmware_test debug
# firmware_test Release — как будет на производстве [doc('Прошить firmware_test Release во Flash')]
[group('flash')] [group('flash')]
flash-test-release: flash-test-release:
@just host::flash firmware_test release @just host::flash firmware_test release
# TODO: bootloader + app Release — финальная прошивка [confirm("Flash bootloader + app (Release)?")]
[group('flash'), confirm("Flash bootloader + app (Release)?")] [doc('Прошить bootloader + tft_app Release во Flash (производство)')]
[group('flash')]
flash-production: flash-production:
@just host::flash bootloader release @just host::flash bootloader release
@just host::flash app release @just host::flash app release
# ============================================================================= # =============================================================================
# ГРУППА: pipeline — производственные сценарии (для сервера) # ГРУППА: hil — запуск HIL-тестов на реальном железе
# Сборка target-прошивок — в devcontainer: just build::build-hil
# ============================================================================= # =============================================================================
# Входной контроль платы: тестовая прошивка (Release) -> HIL-тесты _hil_build := BUILD_DIR / "target-debug"
[doc('Загрузить test_host_uart.elf на таргет без запуска тестов (для ручной отладки)')]
[group('hil')]
hil-load:
HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} python load_and_run.py \
{{ _hil_build }}/tests/target/host_uart/test_host_uart.elf
[doc('Запустить все HIL-тесты')]
[group('hil')]
hil-run:
HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} pytest -v
[doc('Запустить только smoke HIL-тесты (быстро, для CI)')]
[group('hil')]
hil-smoke:
HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} pytest -v -m smoke
[doc('Запустить HIL-тесты без перезагрузки ELF (прошивка уже запущена)')]
[group('hil')]
hil-run-fast:
HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} pytest -v --no-load
# =============================================================================
# ГРУППА: pipeline
# =============================================================================
[doc('Входной контроль: прошить firmware_test Release + запустить HIL-тесты')]
[group('pipeline')] [group('pipeline')]
incoming: flash-test-release incoming: flash-test-release
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
echo " ▶ Running HIL tests (CAN, UART, SDRAM, SPI Flash)..." echo " ▶ Running HIL tests..."
# cd tools/hil && UV_PROJECT_ENVIRONMENT={{_venv}} uv run python run_hil.py <- раскомментить когда готово just host::hil-run
echo " ⚠️ HIL tests not yet implemented"
# Финальная прошивка: загрузчик + основная прошивка (Release) [doc('Производственная прошивка: bootloader + tft_app Release')]
[group('pipeline')] [group('pipeline')]
production: flash-production production: flash-production
@echo " ✅ Production firmware flashed (bootloader + app)" @echo " ✅ Production firmware flashed"
# ============================================================================= # =============================================================================
# ГРУППА: util — вспомогательные инструменты # ГРУППА: util
# ============================================================================= # =============================================================================
# Найти подключённые NXP USB-устройства [doc('Найти подключённые NXP устройства (BootROM, flashloader, обычный режим)')]
[group('util')] [group('util')]
scan: scan:
cd "{{TOOLS_DIR}}" && UV_PROJECT_ENVIRONMENT={{_venv}} uv run nxpdevscan cd "{{ TOOLS_DIR }}" && UV_PROJECT_ENVIRONMENT={{ _venv }} uv run nxpdevscan
# Проверить связь с BootROM через SDP (плата в SDP-режиме) [doc('Получить статус SDP (BootROM режим, VID:PID 1FC9:0130)')]
[group('util'), no-cd] [group('util')]
[no-cd]
sdp-status: sdp-status:
cd "{{TOOLS_DIR}}" && UV_PROJECT_ENVIRONMENT={{_venv}} uv run sdphost -u 0x1FC9,0x0130 -- error-status cd "{{ TOOLS_DIR }}" && UV_PROJECT_ENVIRONMENT={{ _venv }} uv run sdphost -u 0x1FC9,0x0130 -- error-status
# Проверить что Flashloader отвечает (после jump-address) [doc('Получить статус flashloader (VID:PID 15A2:0073)')]
[group('util')] [group('util')]
flashloader-status: flashloader-status:
cd "{{TOOLS_DIR}}" && UV_PROJECT_ENVIRONMENT={{_venv}} uv run blhost -u 0x15A2,0x0073 -- get-property 1 0 cd "{{ TOOLS_DIR }}" && UV_PROJECT_ENVIRONMENT={{ _venv }} uv run blhost -u 0x15A2,0x0073 -- get-property 1 0
# Обновить spsdk до новой версии [doc('Обновить spsdk до указанной версии: upgrade-tools <version>')]
# Использование: just upgrade-tools 3.8.0
[group('util')] [group('util')]
upgrade-tools version: upgrade-tools version:
cd "{{TOOLS_DIR}}" && uv add "spsdk=={{version}}" && uv sync cd "{{ TOOLS_DIR }}" && uv add "spsdk=={{ version }}" && uv sync
@echo " ✅ spsdk upgraded to {{version}}" @echo " ✅ spsdk upgraded to {{ version }}"
@echo " Run: git add tools/host/uv.lock tools/host/pyproject.toml"
# =============================================================================
# ГРУППА: debug
# =============================================================================
_hil_dir := "tools/hil"
[doc('Запустить GDB-сервер на порту 3333. Оставить запущенным, подключаться из VSCode')]
[group('debug')]
debug-server:
uv run --directory {{ _hil_dir }} pyocd gdbserver \
--config {{ justfile_directory() }}/pyocd_debug.yaml \
--port 3333 \
--persist \
--semihosting
[doc('Показать доступные builtin-таргеты pyOCD для MIMXRT')]
[group('debug')]
debug-list-targets:
uv run --directory {{ _hil_dir }} pyocd list --targets | grep -i mimx

10
pyocd_debug.yaml Normal file
View file

@ -0,0 +1,10 @@
# pyocd_debug.yaml — конфигурация для отладки через GDB
#
# Используется: just host::debug-server
# Не использовать для HIL-тестов — для них pyocd.yaml (target: cortex_m, RAM-режим)
#
# Первый запуск — установить CMSIS pack:
# just host::debug-pack-install
target_override: mimxrt1050_quadspi
frequency: 4000000

372
tests/HIL_CREATE_TEST.md Normal file
View file

@ -0,0 +1,372 @@
# Добавление нового HIL-теста
## Обзор стека
```bash
devcontainer хост
──────────────────────────────── ──────────────────────────────────────
tests/target/<name>/ tools/hil/
main.c ← C-прошивка test_<name>.py ← pytest-тесты
CMakeLists.txt conftest.py ← фикстуры (общие)
CMakePresets.json just/host.just
target-debug-build hil-run, hil-smoke ...
└── targets: [test_<name>]
just/build.just
build-hil
```
---
## Шаг 1 — C-прошивка: `tests/target/<name>/`
### `main.c`
Минимальный шаблон для нового теста:
```c
#include "board.h"
#include "bsp/led.h"
#include "bsp/tick.h"
#include "bsp/uart_host.h"
#include <string.h>
#define CLI_BAUD_RATE 115200U
#define CLI_LINE_MAX 128U
#define CLI_RX_TIMEOUT 100U
static size_t cli_read_line(uint8_t *p_buf, size_t max_len)
{
size_t pos = 0U;
while (pos < (max_len - 1U)) {
int32_t byte = bsp_uart_host_read_byte(CLI_RX_TIMEOUT);
if (byte < 0) break;
if ((char)byte == '\r') continue;
if ((char)byte == '\n') break;
p_buf[pos++] = (uint8_t)byte;
}
p_buf[pos] = '\0';
return pos;
}
static void cli_process_line(const char *p_line)
{
if (strncmp(p_line, "PING", 4U) == 0) {
bsp_uart_host_write_str("PONG\r\n");
}
/* TODO: добавить команды для нового теста */
else if (p_line[0] != '\0') {
bsp_uart_host_write_str("ERR_UNKNOWN\r\n");
}
}
int main(void)
{
board_hw_init();
bsp_tick_init();
bsp_led_init();
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);
}
static uint8_t s_line_buf[CLI_LINE_MAX];
for (;;) {
size_t len = cli_read_line(s_line_buf, sizeof(s_line_buf));
if (len > 0U) cli_process_line((const char *)s_line_buf);
}
}
```
### `CMakeLists.txt`
```cmake
set(TARGET_NAME test_<name>)
add_executable(${TARGET_NAME}
main.c
${BSP_GENERATED}/clock_config.c
${BSP_STARTUP_FILE}
${BSP_SYSCALLS_FILE}
)
target_link_options(${TARGET_NAME} PRIVATE
-T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_ram.ld
-Wl,--gc-sections
-Wl,--print-memory-usage
-Wl,-Map=${CMAKE_CURRENT_BINARY_DIR}/${TARGET_NAME}.map
)
target_link_libraries(${TARGET_NAME} PRIVATE
bsp_boot_ram
bsp_board
bsp_led
bsp_tick
bsp_uart_host
# + bsp_can / bsp_sdio / ... если нужно
)
add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${TARGET_NAME}>
COMMENT "Size: ${TARGET_NAME}"
)
```
---
## Шаг 2 — Подключить в `tests/target/CMakeLists.txt`
```cmake
add_subdirectory(host_uart)
add_subdirectory(<name>) # ← добавить строку
```
---
## Шаг 3 — `CMakePresets.json`: добавить таргет в `target-debug-build`
```json
{
"name": "target-debug-build",
"configurePreset": "target-debug",
"targets": [
"test_host_uart",
"test_<name>"
]
}
```
---
## Шаг 4 — `just/build.just`: `build-hil` пересобирает всё автоматически
Ничего менять не нужно — `build-hil` вызывает `cmake --build --preset target-debug-build`,
а пресет уже знает про новый таргет после Шага 3.
```bash
# Проверить что новый таргет собирается:
just build::build-hil
```
---
## Шаг 5 — Python-тест: `tools/hil/test_<name>.py`
```python
"""test_<name>.py — HIL тест <что тестируем>."""
import pytest
from conftest import uart_cmd
class Test<Name>:
@pytest.fixture(autouse=True)
def _setup(self, loaded_<name>, uart):
self.ser = uart
@pytest.mark.smoke
def test_ping(self):
"""Базовая проверка канала."""
assert uart_cmd(self.ser, "PING") == "PONG"
def test_something(self):
"""Описание теста."""
resp = uart_cmd(self.ser, "MY_CMD arg")
assert resp == "EXPECTED"
```
---
## Шаг 6 — `conftest.py`: добавить фикстуру загрузки
```python
@pytest.fixture(scope="module")
def loaded_<name>(request: pytest.FixtureRequest) -> None:
_load_elf(
request,
Path(cfg.BUILD_DIR) / "tests/target/<name>/test_<name>.elf",
)
```
Фикстура `uart` уже зависит от `loaded_host_uart`. Для нового теста нужна
своя пара: `loaded_<name>` + при необходимости своя `uart_<name>` если нужен
отдельный порт или скорость. Обычно достаточно переиспользовать `uart`.
---
## Шаг 7 — `just/host.just`: добавить рецепты (опционально)
Для часто используемых тестов удобно добавить алиасы:
```just
[group('hil')]
hil-run-<name>:
HIL_BUILD_DIR={{_hil_build}} \
uv run --directory {{HIL_DIR}} pytest test_<name>.py -v
```
Если алиас не нужен — `just host::hil-run` запускает **все** тесты из `tools/hil/`
автоматически (pytest обходит все `test_*.py`).
---
## Полный цикл
```bash
# 1. devcontainer — собрать прошивку
just build::build-hil
# 2. хост — запустить все HIL тесты (включая новый)
just host::hil-run
# 3. хост — только новый тест
just host::hil-run-<name>
# 4. хост — загрузить ELF без тестов (для ручной отладки)
uv run --directory tools/hil python load_and_run.py \
build/target-debug/tests/target/<name>/test_<name>.elf
```
---
## Как работают фикстуры
### Общая схема зависимостей
Для каждого тест-модуля цепочка фикстур одна и та же:
```bash
test_foo()
└── _setup (scope=function, autouse)
├── loaded_<n> (scope=module) ← грузит ELF на MCU
└── uart (scope=module) ← открывает порт, ждёт READY
└── depends_on: loaded_<n> ← гарантирует порядок
```
`scope=module` означает: фикстура создаётся один раз на весь файл с тестами
и уничтожается после последнего теста в нём. Все тесты внутри одного файла
разделяют один и тот же экземпляр — ELF загружается один раз, порт открывается
один раз.
### Порядок вызовов внутри одного модуля
```bash
──────────────────────────────────── module scope (один раз на файл)
1. loaded_<n>()
└── _load_elf()
├── open_target() → pyOCD: подключиться к MCU
├── flexram_init() → настроить ITCM/DTCM
├── load_elf() → записать PT_LOAD сегменты по адресам
└── run_from_vectors()→ SP/PC из 0x00000000/0x00000004 → resume
2. uart() (зависит от loaded_<n>, создаётся после)
├── serial.Serial.open()
├── while readline() != "READY": ← ждём сигнал от прошивки
└── yield ser → порт готов к работе
──────────────────────────────────── function scope (каждый тест)
3. _setup()
└── self.ser = uart → просто сохранить ссылку
4. test_ping() → uart_cmd(self.ser, "PING") == "PONG"
5. test_echo_simple()
6. ... остальные тесты
──────────────────────────────────── teardown (в обратном порядке)
7. uart teardown → ser.close()
8. loaded_<n> teardown → (нет, возвращает None)
```
### Почему `uart` явно зависит от `loaded_<n>`
```python
def uart(request, loaded_<n>): # ← зависимость объявлена в сигнатуре
...
```
Без этой зависимости pytest мог бы создать `uart` раньше чем ELF загружен.
Порт бы открылся, но `READY` не пришёл бы — таймаут и падение. Явная
зависимость в сигнатуре — единственный надёжный способ гарантировать порядок.
### Что происходит при запуске нескольких тест-файлов
```bash
pytest test_uart.py test_can.py
```
```bash
test_uart.py test_can.py
────────────────────────────── ──────────────────────────────
loaded_host_uart ← создаётся loaded_can ← создаётся
uart ← создаётся uart ← создаётся заново
test_ping test_can_send
test_echo test_can_receive
uart.close() ← teardown uart.close() ← teardown
```
Каждый файл получает **свою** загрузку ELF и свой сеанс UART. MCU
перезагружается между файлами — это правильно, у каждого теста своя прошивка.
### Что происходит при запуске одного теста из модуля
```bash
uv run pytest test_uart.py::TestUartBasic::test_echo_simple -v
```
Несмотря на то что запущен один тест, `scope=module`-фикстуры всё равно
создаются: ELF загружается, порт открывается, READY ожидается. Это цена за
изоляцию — зато тест полностью самодостаточен.
### Шаблон `conftest.py` для нового теста
Для каждого нового тест-модуля нужно добавить только одну фикстуру — `loaded_<n>`.
Всё остальное (`uart`, `uart_cmd`, `open_target`, `flexram_init`) переиспользуется:
```python
# conftest.py — добавить:
@pytest.fixture(scope="module")
def loaded_<n>(request: pytest.FixtureRequest) -> None:
_load_elf(
request,
Path(cfg.BUILD_DIR) / "tests/target/<n>/test_<n>.elf",
)
# Если нужна отдельная uart-фикстура (другой порт, другой бод):
@pytest.fixture(scope="module")
def uart_<n>(
request: pytest.FixtureRequest,
loaded_<n>, # ← порядок гарантирован
) -> Generator[serial.Serial, None, None]:
port = request.config.getoption("--vcom")
ser = serial.Serial(port=port, baudrate=115200, timeout=2.0)
# ждём READY ...
yield ser
ser.close()
```
В большинстве случаев отдельная `uart_<n>` не нужна — стандартная `uart`
работает для любого теста, потому что протокол (`READY` + текстовые команды)
одинаковый для всех прошивок.
---
## Чеклист
```bash
[ ] tests/target/<name>/main.c — C-прошивка с CLI
[ ] tests/target/<name>/CMakeLists.txt — сборка с bsp_boot_ram
[ ] tests/target/CMakeLists.txt — add_subdirectory(<name>)
[ ] CMakePresets.json — добавить test_<name> в targets
[ ] tools/hil/test_<name>.py — pytest-тесты
[ ] tools/hil/conftest.py — добавить loaded_<name> фикстуру
[ ] just/host.just — алиас hil-run-<name> (опционально)
```

376
tests/HOST_CREATE_TEST.md Normal file
View file

@ -0,0 +1,376 @@
# Добавление нового host-теста
## Обзор стека
```
devcontainer
─────────────────────────────────────────────────────────────────
tests/host/<n>/test_<n>.c ← тест (Unity + опционально fff)
tests/host/CMakeLists.txt ← регистрация через add_host_test()
tests/host/mocks/ ← stub-хедеры NXP SDK (если нужны)
CMakePresets.json just/build.just
host-debug-build test-host
└── targets: [test_<n>] cmake --build + ctest
```
---
## Шаг 0 — Определить категорию теста
Перед написанием кода определи к какой категории относится модуль:
| Категория | Описание | Инструментарий |
|-----------|----------|----------------|
| **A** | Нет вызовов NXP SDK: алгоритмы, парсеры, FSM, структуры данных | Unity |
| **B** | BSP-модуль вызывает `fsl_*.h`, USB-стек и т.д. | Unity + fff + stub-хедеры |
**Признак категории A:** в `.c` файле модуля нет ни одного `#include "fsl_*.h"`.
**Признак категории B:** есть хотя бы один такой include.
---
## Шаг 1 — Создать тестовый файл
### Категория A — платформонезависимый модуль
```c
/* tests/host/<n>/test_<n>.c */
#include "unity.h"
#include "<n>.h" /* тестируемый модуль */
void setUp(void) { /* сброс состояния перед каждым тестом */ }
void tearDown(void) { /* очистка после каждого теста */ }
void test_something(void)
{
/* Arrange */
int input = 42;
/* Act */
int result = module_process(input);
/* Assert */
TEST_ASSERT_EQUAL(expected, result);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_something);
return UNITY_END();
}
```
### Категория B — BSP-модуль с fff-фейками
Порядок `#include` принципиален — нарушение порядка вызовет ошибки компиляции:
```c
/* tests/host/<n>/test_<n>.c */
#include "unity.h"
#include "fff.h"
DEFINE_FFF_GLOBALS; /* 1. ровно один раз на весь .c файл */
#include "fsl_<driver>.h" /* 2. stub-хедер с типами (из mocks/) */
/* 3. объявить фейки для всех SDK-функций, которые вызывает тестируемый модуль */
FAKE_VOID_FUNC(SDK_Function_A, ArgType1, ArgType2);
FAKE_VALUE_FUNC(status_t, SDK_Function_B, ArgType1);
#include "bsp/<module>.h" /* 4. тестируемый модуль — всегда последним */
void setUp(void)
{
RESET_FAKE(SDK_Function_A);
RESET_FAKE(SDK_Function_B);
FFF_RESET_HISTORY();
/* при необходимости задать дефолтные return_val */
}
void tearDown(void) { }
void test_something(void)
{
/* Arrange: настроить поведение фейков */
SDK_Function_B_fake.return_val = kStatus_Success;
/* Act */
bsp_status_t status = bsp_module_do_something();
/* Assert: проверить результат и вызовы */
TEST_ASSERT_EQUAL(BSP_OK, status);
TEST_ASSERT_EQUAL(1, SDK_Function_A_fake.call_count);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_something);
return UNITY_END();
}
```
---
## Шаг 2 — Stub-хедеры (только категория B)
Если тестируемый модуль использует NXP SDK хедеры которых ещё нет в `tests/host/mocks/` — нужно создать stub.
### Что такое stub-хедер и зачем он нужен
NXP SDK хедеры (`fsl_gpio.h` и др.) тянут платформенные регистровые определения для Cortex-M7 — они не компилируются на хосте. Stub-хедер в `tests/host/mocks/` содержит только минимально необходимые типы и сигнатуры функций. CMake подключает `mocks/` **до** SDK, поэтому компилятор находит stub раньше оригинала.
### Шаблон stub-хедера
Добавляй в stub только то, что реально используется в тестируемом `.c` файле:
```c
/* tests/host/mocks/fsl_<driver>.h */
#pragma once
#include <stdint.h>
/* Минимально необходимые типы */
typedef struct { uint32_t reserved[64]; } DRIVER_Type;
typedef enum {
kStatus_Success = 0,
kStatus_Fail = 1,
} status_t;
/* Сигнатуры функций — реализации предоставляет fff */
void SDK_Function_A(DRIVER_Type *base, uint32_t arg);
status_t SDK_Function_B(DRIVER_Type *base, const uint8_t *data, size_t len);
```
### Уже существующие stubs в `tests/host/mocks/`
| Файл | Что заменяет | Используется в |
|------|-------------|----------------|
| `fsl_gpio.h` | GPIO драйвер | `test_bsp_led` |
| `pin_mux.h` | Макросы пинов из `generated/` | `test_bsp_led` |
| `board.h` | `board_hw_init()` | `test_bsp_led` |
Если нужный stub уже есть — ничего создавать не нужно, просто укажи `mocks/` в `MOCKS` аргументе `add_host_test()`.
---
## Шаг 3 — Зарегистрировать тест в `tests/host/CMakeLists.txt`
Используй функцию `add_host_test()`. Она создаёт исполняемый файл и регистрирует его в CTest:
```cmake
# Категория A — без mocks
add_host_test(
NAME test_<n>
SOURCES <n>/test_<n>.c
${PROJECT_SOURCE_DIR}/<path_to_module>/<module>.c
INCLUDES
${PROJECT_SOURCE_DIR}/<path_to_module>/include
)
# Категория B — с mocks
add_host_test(
NAME test_<n>
SOURCES <n>/test_<n>.c
${PROJECT_SOURCE_DIR}/bsp/<module>/src/<module>.c
INCLUDES
${PROJECT_SOURCE_DIR}/bsp/<module>/include
MOCKS
${BSP_MOCKS_DIR} # = tests/host/mocks/
)
```
### Аргументы `add_host_test()`
| Аргумент | Обязателен | Описание |
|----------|-----------|----------|
| `NAME` | ✓ | Имя исполняемого файла и теста в CTest |
| `SOURCES` | ✓ | Тестовый `.c` + исходники тестируемых модулей |
| `INCLUDES` | — | Дополнительные include-пути (для `#include "bsp/led.h"` и т.д.) |
| `MOCKS` | — | Директории со stub-хедерами (подключаются с высшим приоритетом) |
`lib_external` (Unity + fff) подключается автоматически — добавлять не нужно.
---
## Шаг 4 — Добавить таргет в `CMakePresets.json`
```json
{
"name": "host-debug-build",
"configurePreset": "host-debug",
"targets": [
"test_bsp_led",
"test_ring_buffer",
"test_timeout_pattern",
"uart_host_mock_example",
"test_<n>"
]
}
```
То же самое для `host-release-build` если нужен Release-прогон.
---
## Шаг 5 — Запустить
```bash
# Сборка + все тесты одной командой (devcontainer)
just build::test-host
# Только новый тест
ctest --preset host-debug-test -R test_<n> -V
# Напрямую — виден полный вывод Unity без CTest-обёртки
./build/host-debug/tests/host/test_<n>
```
---
## Справочник: Unity assertions
```c
/* Целые числа */
TEST_ASSERT_EQUAL(expected, actual)
TEST_ASSERT_EQUAL_INT8 / INT16 / INT32 / UINT8 / UINT32(expected, actual)
TEST_ASSERT_NOT_EQUAL(expected, actual)
TEST_ASSERT_INT_WITHIN(delta, expected, actual)
/* Булевые / указатели */
TEST_ASSERT_TRUE(condition)
TEST_ASSERT_FALSE(condition)
TEST_ASSERT_NULL(pointer)
TEST_ASSERT_NOT_NULL(pointer)
/* Строки / память */
TEST_ASSERT_EQUAL_STRING(expected, actual)
TEST_ASSERT_EQUAL_MEMORY(expected, actual, len)
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, actual, len)
/* Явный провал / пропуск */
TEST_FAIL_MESSAGE("причина")
TEST_IGNORE_MESSAGE("в процессе")
```
---
## Справочник: fff-фейки
### Объявление
```c
FAKE_VOID_FUNC(func, ArgType1, ArgType2); /* void-функция */
FAKE_VALUE_FUNC(RetType, func, ArgType1, ArgType2);/* с возвращаемым значением */
FAKE_VOID_FUNC_VARARG(func, const char *, ...); /* variadic */
```
### Управление поведением
```c
/* Фиксированное возвращаемое значение */
func_fake.return_val = kStatus_Fail;
/* Последовательность значений */
status_t seq[] = {kStatus_Success, kStatus_Success, kStatus_Fail};
SET_RETURN_SEQ(func, seq, 3);
/* Кастомная реализация — высший приоритет, перекрывает return_val */
func_fake.custom_fake = my_impl;
```
### Проверка вызовов
```c
TEST_ASSERT_EQUAL(2, func_fake.call_count); /* сколько раз вызвана */
TEST_ASSERT_EQUAL(expected, func_fake.arg0_val); /* аргумент последнего вызова */
TEST_ASSERT_EQUAL_PTR(func, fff.call_history[0]); /* порядок вызовов */
TEST_ASSERT_EQUAL(0, func_fake.call_count); /* не была вызвана */
```
### Сброс в setUp
```c
void setUp(void)
{
RESET_FAKE(func_a); /* сбрасывает счётчик, историю, return_val, custom_fake */
RESET_FAKE(func_b);
FFF_RESET_HISTORY(); /* сбрасывает глобальную историю порядка вызовов */
}
```
---
## Ловушки
**Dangling pointer из `arg_history[]`.**
`arg_history[]` хранит указатели, не копии. Если функция получала указатель на стековую переменную — после возврата это UB. Использовать `custom_fake` с копированием по значению:
```c
static gpio_pin_config_t s_captured;
static void capture(GPIO_Type *base, uint32_t pin, const gpio_pin_config_t *cfg)
{
s_captured = *cfg; /* копия по значению пока стек ещё жив */
}
void setUp(void) {
RESET_FAKE(GPIO_PinInit);
GPIO_PinInit_fake.custom_fake = capture;
bsp_led_init();
}
void test_init_output(void) {
TEST_ASSERT_EQUAL(kGPIO_DigitalOutput, s_captured.direction);
}
```
**`static` функции не мокаются.**
FFF не видит `static` функции снаружи translation unit. Решение — compile-time seam:
```c
#ifdef UNIT_TEST
void internal_fn(void); /* тест подставит свою реализацию */
#else
static void internal_fn(void) { ... }
#endif
```
**Отсутствие стандартных хедеров в BSP.**
BSP-модули должны явно включать `<stdint.h>`, `<stdbool.h>`, `<stddef.h>` — не полагаться на транзитивное подтягивание через NXP SDK. На хосте этот транзит отсутствует, компиляция упадёт с `undeclared identifier 'size_t'`.
---
## Полный цикл
```bash
# 1. Создать тестовый файл
tests/host/<n>/test_<n>.c
# 2. Создать stub-хедер (если категория B и stub не существует)
tests/host/mocks/fsl_<driver>.h
# 3. Добавить вызов add_host_test() в
tests/host/CMakeLists.txt
# 4. Добавить "test_<n>" в targets в
CMakePresets.json ← host-debug-build и host-release-build
# 5. Запустить в devcontainer
just build::test-host
```
---
## Чеклист
```
[ ] Определена категория (A или B)
[ ] tests/host/<n>/test_<n>.c — тест с main(), setUp(), tearDown()
[ ] tests/host/mocks/fsl_<driver>.h — stub (только категория B, если нет)
[ ] tests/host/CMakeLists.txt — add_host_test(NAME test_<n> ...)
[ ] CMakePresets.json — добавить test_<n> в host-debug-build
[ ] just build::test-host — зелёный прогон
```

View file

@ -3,10 +3,9 @@
# ============================================================================= # =============================================================================
set(TARGET_NAME test_host_uart) set(TARGET_NAME test_host_uart)
set(BSP_GENERATED ${CMAKE_SOURCE_DIR}/bsp/generated)
add_executable(${TARGET_NAME} main.c ${BSP_GENERATED}/clock_config.c add_executable(${TARGET_NAME} main.c ${BSP_GENERATED}/clock_config.c
${BSP_GENERATED}/startup/startup_MIMXRT1052.S) ${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE})
target_compile_definitions(${TARGET_NAME} target_compile_definitions(${TARGET_NAME}
PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512) PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512)

View file

@ -100,11 +100,19 @@ int main(void)
bsp_tick_init(); bsp_tick_init();
bsp_led_init(); bsp_led_init();
bsp_uart_host_init(CLI_BAUD_RATE); bsp_uart_host_init(CLI_BAUD_RATE);
/* pytest ждёт эту строку как сигнал готовности */
bsp_uart_host_write_str("READY\r\n");
bsp_led_on(LED_HEARTBEAT); bsp_led_on(LED_HEARTBEAT);
/* Шлём READY каждые 200 мс пока хост не откроет порт и не пришлёт байт.
* Как только в RX-буфере что-то появится переходим в основной цикл.
* Это убирает race condition между загрузкой ELF и открытием UART. */
while (bsp_uart_host_rx_available() == 0U)
{
bsp_uart_host_write_str("READY\r\n");
bsp_led_toggle(LED_APP);
bsp_delay(200U);
}
bsp_led_off(LED_APP);
static uint8_t s_line_buf[CLI_LINE_MAX]; static uint8_t s_line_buf[CLI_LINE_MAX];
for (;;) for (;;)

View file

@ -1,12 +1,12 @@
""" """
conftest.py pytest-фикстуры для HIL-тестов MIMXRT1052. conftest.py pytest-фикстуры для HIL-тестов MIMXRT1052.
Вся работа с железом делегирована pyocd_utils. Вся работа с железом делегирована pyocd_utils.
Конфигурация читается из env_config (приоритет: env > .env > default).
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
import os
import time import time
from pathlib import Path from pathlib import Path
from typing import Generator from typing import Generator
@ -14,28 +14,19 @@ from typing import Generator
import pytest import pytest
import serial import serial
import env_config as cfg
from pyocd_utils import flexram_init, load_elf, open_target, run_from_vectors from pyocd_utils import flexram_init, load_elf, open_target, run_from_vectors
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Конфигурация — переопределяется через переменные окружения
# ---------------------------------------------------------------------------
_REPO_ROOT = Path(__file__).resolve().parents[3]
_BUILD_DIR = Path(os.environ.get("HIL_BUILD_DIR",
_REPO_ROOT / "build" / "target-debug"))
_VCOM_PORT = os.environ.get("HIL_VCOM_PORT", "/dev/tty.usbmodemGUXFBWDJBWTGQ3")
_VCOM_BAUD = int(os.environ.get("HIL_VCOM_BAUD", "115200"))
_READY_TIMEOUT = float(os.environ.get("HIL_READY_TIMEOUT", "5.0"))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# CLI-опции pytest # CLI-опции pytest (перекрывают .env и os.environ)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def pytest_addoption(parser: pytest.Parser) -> None: def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption("--elf", default=None, help="Путь к .elf файлу") parser.addoption("--elf", default=None, help="Путь к .elf файлу")
parser.addoption("--vcom", default=_VCOM_PORT, help="VCOM-порт MCU-Link") parser.addoption("--vcom", default=cfg.VCOM_PORT, help="VCOM-порт MCU-Link")
parser.addoption("--no-load", action="store_true", help="ELF уже запущен") parser.addoption("--no-load", action="store_true", help="ELF уже запущен")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -49,7 +40,7 @@ def _load_elf(request: pytest.FixtureRequest, default_elf: Path) -> None:
elf = Path(request.config.getoption("--elf") or default_elf) elf = Path(request.config.getoption("--elf") or default_elf)
assert elf.exists(), f"ELF не найден: {elf}" assert elf.exists(), f"ELF не найден: {elf}"
with open_target() as target: with open_target(frequency=cfg.PYOCD_FREQUENCY) as target:
flexram_init(target) flexram_init(target)
load_elf(target, str(elf)) load_elf(target, str(elf))
run_from_vectors(target) run_from_vectors(target)
@ -60,16 +51,12 @@ def _load_elf(request: pytest.FixtureRequest, default_elf: Path) -> None:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Фикстуры загрузки (scope=module — один раз на файл с тестами) # Фикстуры загрузки (scope=module — один раз на файл с тестами)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@pytest.fixture(scope="module")
def loaded_firmware_test(request: pytest.FixtureRequest) -> None:
_load_elf(request, _BUILD_DIR / "firmware/test/firmware_test.elf")
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def loaded_host_uart(request: pytest.FixtureRequest) -> None: def loaded_host_uart(request: pytest.FixtureRequest) -> None:
_load_elf( _load_elf(
request, request,
_BUILD_DIR / "tests/target/host_uart/test_host_uart.elf", Path(cfg.BUILD_DIR) / "tests/target/host_uart/test_host_uart.elf",
) )
@ -79,21 +66,21 @@ def loaded_host_uart(request: pytest.FixtureRequest) -> None:
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def uart( def uart(
request: pytest.FixtureRequest, request: pytest.FixtureRequest,
loaded_host_uart, # гарантирует порядок: сначала загрузка loaded_host_uart,
) -> Generator[serial.Serial, None, None]: ) -> Generator[serial.Serial, None, None]:
port = request.config.getoption("--vcom") port = request.config.getoption("--vcom")
log.info("UART %s @ %d baud", port, _VCOM_BAUD) log.info("UART %s @ %d baud", port, cfg.VCOM_BAUD)
ser = serial.Serial( ser = serial.Serial(
port=port, port=port,
baudrate=_VCOM_BAUD, baudrate=cfg.VCOM_BAUD,
timeout=2.0, timeout=2.0,
write_timeout=1.0, write_timeout=1.0,
) )
# Ждём "READY" — прошивка отправляет его один раз при старте # Ждём READY
deadline = time.monotonic() + _READY_TIMEOUT deadline = time.monotonic() + cfg.READY_TIMEOUT
ready = False ready = False
while time.monotonic() < deadline: while time.monotonic() < deadline:
line = ser.readline().decode("ascii", errors="replace").strip() line = ser.readline().decode("ascii", errors="replace").strip()
@ -105,8 +92,8 @@ def uart(
if not ready: if not ready:
ser.close() ser.close()
pytest.fail( pytest.fail(
f"Прошивка не отправила READY за {_READY_TIMEOUT} с. " f"Прошивка не отправила READY за {cfg.READY_TIMEOUT} с "
"Проверьте VCOM-порт и bsp_uart_host_init()." "проверьте VCOM-порт и bsp_uart_host_init()."
) )
ser.reset_input_buffer() ser.reset_input_buffer()

26
tools/hil/env_config.py Normal file
View file

@ -0,0 +1,26 @@
"""
env_config.py конфигурация HIL из переменных окружения.
При запуске через just (set dotenv-load + set export) все переменные
из корневого .env автоматически попадают в os.environ до запуска pytest.
При прямом запуске pytest (без just) выставить переменные вручную
или через `export HIL_VCOM_PORT=...` перед запуском.
"""
from __future__ import annotations
import os
from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parents[2]
def _get(key: str, default: str) -> str:
return os.environ.get(key, default)
BUILD_DIR: str = _get("HIL_BUILD_DIR",
str(_REPO_ROOT / "build" / "target-debug"))
VCOM_PORT: str = _get("HIL_VCOM_PORT", "/dev/ttyACM0")
VCOM_BAUD: int = int(_get("HIL_VCOM_BAUD", "115200"))
READY_TIMEOUT: float = float(_get("HIL_READY_TIMEOUT", "5.0"))
PYOCD_FREQUENCY: int = int(_get("HIL_PYOCD_FREQUENCY", "1000000"))

View file

@ -1,7 +1,7 @@
[project] [project]
name = "hil" name = "hil"
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "HIL-тесты для MIMXRT1052 — pytest + pyOCD + pyserial"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
@ -23,4 +23,4 @@ markers = [
log_cli = true log_cli = true
log_cli_level = "INFO" log_cli_level = "INFO"
log_format = "%(asctime)s [%(levelname)-8s] %(name)s: %(message)s" log_format = "%(asctime)s [%(levelname)-8s] %(name)s: %(message)s"
log_date_format = "%H:%M:%S" log_date_format = "%H:%M:%S"

1334
tools/host/uv.lock Normal file

File diff suppressed because it is too large Load diff