# 14 - Шаблон HIL-теста
- Создан первый рабочий, шаблонный HIL-тест (bsp_opto) - актуализирована вся документация
This commit is contained in:
parent
f037bea91f
commit
59b9ff55f7
35 changed files with 2189 additions and 1965 deletions
10
.env.example
10
.env.example
|
|
@ -1,10 +1,7 @@
|
|||
# =============================================================================
|
||||
# .env — единый источник конфигурации проекта
|
||||
# Читается: just (set dotenv-load) → экспортируется в окружение (set export)
|
||||
# → наследуется uv run / python3 автоматически
|
||||
#
|
||||
# Скопируйте в .env и настройте под свою машину:
|
||||
# cp .env.example .env
|
||||
# → наследуется uv run python3 автоматически
|
||||
# =============================================================================
|
||||
|
||||
# --- Hardware ---
|
||||
|
|
@ -31,6 +28,7 @@ TARGET_CFG=target/imxrt.cfg
|
|||
GDB_PORT=3333
|
||||
GDB_EXECUTABLE=arm-none-eabi-gdb
|
||||
|
||||
|
||||
# pyOCD — таргет и частота для gdbserver и flash_swd.py
|
||||
PYOCD_TARGET=mimxrt1050_quadspi
|
||||
PYOCD_FREQUENCY=4000000
|
||||
|
|
@ -40,8 +38,8 @@ FCB_PATH=tools/host/dcd/w25q128_fdcb.bin
|
|||
|
||||
# --- HIL (аппаратный стенд) ---
|
||||
# Порты: macOS = /dev/cu.usbmodem*, Linux = /dev/ttyACM*
|
||||
HIL_VCOM_PORT=/dev/ttyACM0
|
||||
HIL_M5_PORT=/dev/ttyACM1
|
||||
HIL_VCOM_PORT=/dev/cu.usbmodemGUXFBWDJBWTGQ3
|
||||
HIL_M5_PORT=/dev/cu.usbmodem211101
|
||||
HIL_VCOM_BAUD=115200
|
||||
HIL_M5_BAUD=115200
|
||||
HIL_READY_TIMEOUT=5.0
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -57,6 +57,7 @@ Debug/
|
|||
Release/
|
||||
build*
|
||||
Testing/
|
||||
!docs/testing
|
||||
!build.just
|
||||
|
||||
# Файлы настройки среды разработки
|
||||
|
|
|
|||
214
README.md
214
README.md
|
|
@ -2,171 +2,87 @@
|
|||
|
||||
Монорепозиторий для **MIMXRT1052CVJ5B**. Содержит три независимых firmware-проекта с общей инфраструктурой сборки, тестирования и инструментарием.
|
||||
|
||||
> Архитектура рабочего окружения разработчика — [docs/DEV_ARCH.md](docs/DEV_ARCH.md)
|
||||
|
||||
---
|
||||
|
||||
## Структура репозитория
|
||||
|
||||
```bash
|
||||
/
|
||||
├── .devcontainer/ # VSCode Devcontainer — единое окружение для всех разработчиков
|
||||
│ ├── Dockerfile
|
||||
│ └── devcontainer.json
|
||||
├── .vscode/
|
||||
│ ├── launch.json
|
||||
│ └── tasks.json # UI для just build::* (внутри devcontainer)
|
||||
├── bsp/ # Board Support Package
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── common/ # Общие типы (bsp_status_t и др.)
|
||||
│ ├── generated/ # Сгенерировано NXP Config Tools (Pins + Clocks Tool)
|
||||
│ │ ├── TFT_Board.mex # Источник истины конфигурации пинов и тактирования
|
||||
│ │ ├── pin_mux.c/h # Сгенерировано из .mex (Pins Tool)
|
||||
│ │ ├── clock_config.c/h # Сгенерировано из .mex (Clocks Tool)
|
||||
│ │ ├── board.c/h # Ручная инициализация специфики платы
|
||||
│ │ ├── 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_host.cmake # Host GCC для unit-тестов
|
||||
├── sdk/ # NXP MCUXpresso SDK — vendored
|
||||
├── lib/ # Внешние библиотеки — vendored
|
||||
│ ├── Unity/ # Фреймворк для unit-тестов
|
||||
│ ├── fff/ # Fake Function Framework для моков
|
||||
│ └── SEGGER/ # SEGGER RTT — вывод логов через отладчик
|
||||
├── firmware/
|
||||
│ ├── test/ # [Проект 1] Тестовая прошивка — входной контроль платы
|
||||
│ ├── bootloader/ # [Проект 2] Загрузчик с поддержкой A/B обновления
|
||||
│ └── tft_app/ # [Проект 3] Основная боевая прошивка (FreeRTOS)
|
||||
├── tests/
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── host/ # Unit-тесты на хостовом компиляторе (Unity + fff)
|
||||
│ │ ├── mocks/ # Stub-хедеры NXP SDK для компиляции на хосте
|
||||
│ │ ├── 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/
|
||||
│ ├── host/ # Инструменты прошивки (spsdk)
|
||||
│ │ ├── flash_usb.py # Прошивка через USB ROM (sdphost + blhost)
|
||||
│ │ ├── hab/ # HAB yaml-конфиги для nxpimage
|
||||
│ │ ├── dcd/ # ivt_flashloader.bin, dcd.bin
|
||||
│ │ ├── pyproject.toml
|
||||
│ │ └── uv.lock
|
||||
│ └── hil/ # HIL-тесты (pytest + pyOCD + pyserial)
|
||||
│ ├── pyproject.toml
|
||||
│ ├── 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 пайплайны
|
||||
├── docs/
|
||||
│ ├── DEV_ARCH.md
|
||||
│ ├── CMAKE_HINTS.md
|
||||
│ ├── HOW_TO_FLASH.md
|
||||
│ ├── BOOT_FLAGS.md # Флаги загрузчика
|
||||
│ └── schematic.pdf # Схема платы
|
||||
├── pyocd.yaml # Конфигурация pyOCD (target: cortex_m, RAM-режим)
|
||||
├── .env # Конфигурация проекта (VID:PID, HIL-порты и др.)
|
||||
├── .env.example # Шаблон .env для новых разработчиков
|
||||
├── bootstrap.sh # Первичная настройка окружения (уровень 0)
|
||||
├── CMakeLists.txt # Корневой CMake
|
||||
├── CMakePresets.json # Пресеты сборки (Debug/Release/Host/Target)
|
||||
└── justfile # Точка входа для команд (модули: build, host, ci)
|
||||
```
|
||||
> Архитектура рабочего окружения — [docs/DEV_ARCH.md](docs/DEV_ARCH.md)
|
||||
|
||||
---
|
||||
|
||||
## Три firmware-проекта
|
||||
|
||||
### 1. Тестовая прошивка (`firmware/test/`)
|
||||
| Проект | Путь | Описание |
|
||||
|--------|------|----------|
|
||||
| Тестовая прошивка | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS |
|
||||
| Загрузчик | `firmware/bootloader/` | A/B обновление через uSD. Сам обновляется только через USB ROM + blhost |
|
||||
| Боевая прошивка | `firmware/tft_app/` | FreeRTOS + FatFS + бизнес-логика. Обновляется загрузчиком |
|
||||
|
||||
Bare-metal прошивка для **входного контроля** платы. Проверяет базовую работоспособность всех интерфейсов: CAN, UART, SDRAM, QSPI Flash, uSD (SDIO), RGB-интерфейс, гальванически развязанные входы, светодиоды, кнопки, IR-приёмник, MQS.
|
||||
---
|
||||
|
||||
Загружается через USB ROM (SDP) — подробнее в [docs/HOW_TO_FLASH.md](docs/HOW_TO_FLASH.md).
|
||||
## BSP
|
||||
|
||||
### 2. Загрузчик (`firmware/bootloader/`)
|
||||
|
||||
Отвечает за обновление боевой прошивки в полевых условиях. Поддерживает схему **A/B** с обновлением через uSD. Обновление самого загрузчика — только через USB ROM + blhost, не через себя.
|
||||
|
||||
### 3. Боевая прошивка (`firmware/tft_app/`)
|
||||
|
||||
Основная прошивка на базе **FreeRTOS**. Включает FatFS, бизнес-логику, модули. Обновляется через загрузчик по схеме A/B.
|
||||
| Модуль | Путь | Описание |
|
||||
|--------|------|----------|
|
||||
| `bsp_led` | `bsp/led/` | Два UserLed (GPIO3[3], GPIO3[4]) |
|
||||
| `bsp_tick` | `bsp/tick/` | SysTick / FreeRTOS-совместимый таймер |
|
||||
| `bsp_uart_host` | `bsp/uart_host/` | LPUART1 — MCU-Link VCOM (J2) |
|
||||
| `bsp_opto` | `bsp/opto/` | Оптоизолированные входы PS2801-4: EXT_IN1, EXT_IN2, RS_RX |
|
||||
| `bsp_usb_cdc` | `bsp/usb_cdc/` | USB CDC ACM |
|
||||
| generated | `bsp/generated/` | NXP Config Tools: pin_mux, clock_config, board, startup |
|
||||
|
||||
---
|
||||
|
||||
## Тестирование
|
||||
|
||||
Стратегия тестирования трёхуровневая:
|
||||
| Уровень | Где | Инструменты | Запуск |
|
||||
|---------|-----|-------------|--------|
|
||||
| Host unit-тесты | `tests/host/` | Unity + fff, clang | `just build::test-host` (devcontainer) |
|
||||
| HIL target-тесты | `tests/target/` + `tools/hil/` | pyOCD + pyserial + pytest + M5StampPLC | `just host::hil-run` (хост) |
|
||||
|
||||
| Уровень | Расположение | Инструменты | Запуск |
|
||||
|---------|-------------|-------------|--------|
|
||||
| **Host-тесты** (unit) | `tests/host/` | Unity + fff | `just build::test-host` в devcontainer |
|
||||
| **HIL target-тесты** (аппаратные) | `tests/target/` + `tools/hil/` | pyOCD + pyserial + pytest | `just host::hil-run` на хосте |
|
||||
**Host-тесты** запускаются в devcontainer без железа. BSP-модули тестируются через fff-фейки и stub-хедеры.
|
||||
|
||||
### 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.
|
||||
**HIL-тесты** — каждый тест это пара: C-прошивка с UART CLI (`tests/target/<n>/`) и pytest-файл (`tools/hil/test_<n>.py`). pyOCD загружает ELF в RAM через MCU-Link. Тесты с внешними сигналами управляются через M5StampPLC (реле → оптовходы таргета).
|
||||
|
||||
```bash
|
||||
pytest → uart_cmd("PING\r\n") → MCU-Link VCOM → RT1052 → "PONG\r\n" → pytest
|
||||
pytest → uart_cmd() → MCU-Link VCOM → RT1052
|
||||
pytest → m5.opto_set() → M5StampPLC RLY → EXT_IN1/IN2/RS_RX → RT1052
|
||||
```
|
||||
|
||||
Гайд по добавлению нового теста — [tests/HIL_CREATE_TEST.md](tests/HIL_CREATE_TEST.md).
|
||||
- Как добавить host-тест — [tests/HOST_CREATE_TEST.md](tests/HOST_CREATE_TEST.md)
|
||||
- Как добавить HIL-тест — [docs/testing/hil/HIL_CREATE_TEST.md](docs/testing/hil/HIL_CREATE_TEST.md)
|
||||
- Как запустить HIL-тесты — [docs/testing/hil/HIL_HOWTO.md](docs/testing/hil/HIL_HOWTO.md)
|
||||
- HIL стенд и подключение — [docs/testing/hil/HIL_BENCH.md](docs/testing/hil/HIL_BENCH.md)
|
||||
|
||||
---
|
||||
|
||||
## Управление зависимостями
|
||||
## Сборка и прошивка
|
||||
|
||||
| Зависимость | Подход | Причина |
|
||||
|-------------|--------|---------|
|
||||
| NXP MCUXpresso SDK | vendored | Стабильная версия, обновлений не планируется |
|
||||
| FreeRTOS, FatFS, LittleFS и др. | vendored (через SDK) | Стабильные версии |
|
||||
| Unity + fff | vendored | Маленькие, стабильные |
|
||||
| SEGGER RTT | vendored | Стабильный |
|
||||
| pyOCD, pyserial, pytest | `tools/hil/uv.lock` | Фиксированные версии |
|
||||
| spsdk (nxpimage, blhost) | `tools/host/uv.lock` | Фиксированные версии |
|
||||
```bash
|
||||
# devcontainer
|
||||
just build::test-host # host unit-тесты
|
||||
just build::build-firmware-test-debug # ELF
|
||||
just build::hab-firmware-test-debug # HAB-образ для прошивки
|
||||
just build::build-hil # HIL target-прошивки
|
||||
|
||||
**Принцип:** всё что не меняется — vendored. Полностью автономная сборка после `git clone` без доступа к интернету (кроме Python-зависимостей).
|
||||
# хост
|
||||
just host::flash-test-debug # прошить через USB SDP
|
||||
just host::flash-swd-test-debug # прошить через SWD (power cycle после)
|
||||
just host::hil-run # HIL-тесты
|
||||
just host::debug-server # GDB-сервер для отладки
|
||||
```
|
||||
|
||||
Прошивка подробно — [docs/HOW_TO_FLASH.md](docs/HOW_TO_FLASH.md)
|
||||
Отладка подробно — [docs/HOW_TO_DEBUG.md](docs/HOW_TO_DEBUG.md)
|
||||
|
||||
---
|
||||
|
||||
## Devcontainer — состав окружения
|
||||
## Зависимости
|
||||
|
||||
| Инструмент | Назначение |
|
||||
|------------|------------|
|
||||
| `arm-none-eabi-gcc` | Сборка firmware и HIL target-прошивок для ARM |
|
||||
| `arm-none-eabi-gdb` | Отладка через GDB server |
|
||||
| `gcc` / `clang` (host) | Сборка и запуск host-тестов |
|
||||
| `CMake + Ninja` | Система сборки |
|
||||
| `CTest` | Запуск host-тестов |
|
||||
| `clangd` | Language server для VSCode |
|
||||
| `clang-format` | Форматирование кода |
|
||||
| `clang-tidy` | Статический анализ |
|
||||
| `Python 3 + nxp-spsdk` | HAB-образы (nxpimage) |
|
||||
| `just` | Запуск рецептов через модули `build::` |
|
||||
| | Подход |
|
||||
|--|--------|
|
||||
| NXP MCUXpresso SDK, FreeRTOS, FatFS, LittleFS | vendored |
|
||||
| Unity, fff, SEGGER RTT | vendored |
|
||||
| pyOCD, pyserial, pytest, mpremote | `tools/hil/uv.lock` |
|
||||
| spsdk (nxpimage, blhost, sdphost) | `tools/host/uv.lock` |
|
||||
|
||||
Всё что не меняется — vendored. Сборка работает после `git clone` без интернета (кроме Python-зависимостей).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -176,17 +92,21 @@ pytest → uart_cmd("PING\r\n") → MCU-Link VCOM → RT1052 → "PONG\r\n" →
|
|||
git clone <repo-url>
|
||||
cd tft_manufacture_test
|
||||
|
||||
# Инициализация хоста (один раз)
|
||||
# 1. Инициализация хоста (один раз)
|
||||
./bootstrap.sh
|
||||
|
||||
# Открыть в VSCode → Reopen in Container
|
||||
# Затем внутри devcontainer:
|
||||
just build::test-host # host unit-тесты
|
||||
just build::build-firmware-test-debug # сборка firmware
|
||||
just build::hab-firmware-test-debug # подготовка HAB-образа
|
||||
just build::build-hil # сборка HIL target-прошивок
|
||||
# 2. Заполнить .env (порты MCU-Link и M5StampPLC)
|
||||
cp .env.example .env
|
||||
|
||||
# На хосте (вне контейнера):
|
||||
just flash # прошить firmware_test debug во Flash
|
||||
just host::hil-run # загрузить HIL ELF + запустить pytest
|
||||
# 3. Задеплоить агент на M5StampPLC (один раз)
|
||||
just host::m5-deploy
|
||||
|
||||
# 4. Открыть в VSCode → Reopen in Container
|
||||
# Затем внутри devcontainer:
|
||||
just build::test-host
|
||||
just build::build-firmware-test-debug
|
||||
|
||||
# 5. На хосте:
|
||||
just host::flash-test-debug
|
||||
just host::hil-run
|
||||
```
|
||||
|
|
|
|||
135
bsp/README.md
135
bsp/README.md
|
|
@ -1,6 +1,6 @@
|
|||
# BSP — Board Support Package
|
||||
|
||||
> Целевая платформа: NXP IMXRT1052CVJ5B
|
||||
> Целевая платформа: NXP MIMXRT1052CVJ5B
|
||||
> Используется в: `firmware/bootloader`, `firmware/test`, `firmware/tft_app`
|
||||
|
||||
---
|
||||
|
|
@ -14,12 +14,12 @@ firmware/test firmware/bootloader firmware/tft_app
|
|||
↓ ↓ ↓
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ BSP │
|
||||
│ bsp_usb_cdc bsp_uart bsp_can bsp_sdram ... │
|
||||
│ bsp_led bsp_opto bsp_tick bsp_uart_host ... │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
↓ ↓ ↓
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ NXP SDK / middleware │
|
||||
│ fsl_lpuart fsl_flexcan usb stack ... │
|
||||
│ fsl_lpuart fsl_gpio fsl_iomuxc ... │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
|
|
@ -29,26 +29,25 @@ firmware/test firmware/bootloader firmware/tft_app
|
|||
|
||||
```bash
|
||||
bsp/
|
||||
├── CMakeLists.txt # корневой: add_subdirectory для всех компонентов
|
||||
├── CMakeLists.txt # корневой: bsp_board + add_subdirectory для компонентов
|
||||
├── README.md # этот файл
|
||||
│
|
||||
├── generated/ # ← MCUXpresso Config Tools, не редактировать руками
|
||||
│ ├── board.c / board.h
|
||||
│ ├── clock_config.c / clock_config.h
|
||||
│ ├── pin_mux.c / pin_mux.h
|
||||
│ ├── peripherals.c / peripherals.h
|
||||
│ ├── syscalls.c
|
||||
│ ├── TFT_Board.mex # ← источник истины, открывать в Config Tools
|
||||
│ └── startup/
|
||||
│ └── startup_MIMXRT1052.S
|
||||
│
|
||||
├── usb_cdc/ # USB CDC ACM (Virtual COM Port)
|
||||
├── uart/ # LPUART: TTL + изолированный RX +24V
|
||||
├── can/ # FlexCAN
|
||||
├── sdram/ # SEMC → SDRAM 32 MB (MT48LC16M16)
|
||||
├── qspi/ # FlexSPI → W25Q128 (QSPI Flash)
|
||||
├── sdio/ # uSDHC → uSD слот
|
||||
├── display/ # eLCDIF → RGB888
|
||||
├── gpio/ # кнопки, LED, гальванически развязанные входы
|
||||
└── mqs/ # MQS → аналоговый аудио выход
|
||||
├── common/ # bsp_status_t и общие типы
|
||||
├── led/ # bsp_led — два UserLed (GPIO3[3], GPIO3[4])
|
||||
├── tick/ # bsp_tick — SysTick / FreeRTOS-совместимый таймер
|
||||
├── uart_host/ # bsp_uart_host — LPUART1 (MCU-Link VCOM, J2)
|
||||
│ └── mocks/ # fff-заглушки для host-тестов
|
||||
├── opto/ # bsp_opto — оптоизолированные входы PS2801-4
|
||||
└── usb_cdc/ # bsp_usb_cdc — USB CDC ACM
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -57,49 +56,38 @@ bsp/
|
|||
|
||||
Каждый компонент — отдельная статическая библиотека `bsp_<name>`.
|
||||
|
||||
### bsp_board — фундамент, от которого зависят все остальные
|
||||
### bsp_board — фундамент
|
||||
|
||||
```cmake
|
||||
target_link_libraries(bsp_<любой_компонент> PUBLIC bsp_board)
|
||||
```
|
||||
|
||||
`bsp_board` содержит: стартап, clock config, pin mux, board init. Формируется из `generated/` и не должен меняться руками — только через MCUXpresso Config Tools с последующей перегенерацией.
|
||||
Содержит стартап, clock config, pin mux, board init. Формируется из `generated/`
|
||||
и не должен меняться руками — только через MCUXpresso Config Tools.
|
||||
|
||||
### Boot-сценарии — INTERFACE-библиотеки
|
||||
|
||||
Каждая прошивка выбирает один сценарий исполнения кода:
|
||||
### Boot-стратегии — INTERFACE-библиотеки
|
||||
|
||||
| Таргет CMake | Сценарий | Кто использует |
|
||||
|---|---|---|
|
||||
| `bsp_boot_xip` | XIP — код исполняется из Flash | `firmware/test`, `firmware/tft_app` |
|
||||
| `bsp_boot_itcm` | копирование в ITCM | `firmware/bootloader` |
|
||||
| `bsp_boot_sdram` | копирование в SDRAM | зарезервировано |
|
||||
| `bsp_boot_xip` | XIP — исполнение из Flash | `firmware/test`, `firmware/tft_app` |
|
||||
| `bsp_boot_ram` | исполнение из ITCM/DTCM | HIL target-прошивки (`tests/target/`) |
|
||||
|
||||
Подключается явно в каждом проекте:
|
||||
|
||||
```cmake
|
||||
target_link_libraries(firmware_test PRIVATE bsp_board bsp_boot_xip ...)
|
||||
target_link_libraries(test_hil_opto PRIVATE bsp_board bsp_boot_ram ...)
|
||||
```
|
||||
|
||||
### Компоненты периферии
|
||||
|
||||
Каждый компонент подключается независимо — прошивка линкует только то что использует:
|
||||
|
||||
```cmake
|
||||
# firmware/test — использует всё
|
||||
target_link_libraries(firmware_test PRIVATE
|
||||
bsp_board bsp_boot_xip
|
||||
bsp_usb_cdc bsp_uart bsp_can
|
||||
bsp_sdram bsp_qspi bsp_sdio
|
||||
bsp_rtc bsp_display bsp_gpio bsp_ir
|
||||
)
|
||||
|
||||
# firmware/bootloader — минимальный набор
|
||||
target_link_libraries(bootloader PRIVATE
|
||||
bsp_board bsp_boot_itcm
|
||||
bsp_usb_cdc bsp_qspi bsp_uart
|
||||
)
|
||||
```
|
||||
| Библиотека | Модуль | README |
|
||||
|---|---|---|
|
||||
| `bsp_led` | `led/` | [led/README.md](led/README.md) |
|
||||
| `bsp_tick` | `tick/` | [tick/README.md](tick/README.md) |
|
||||
| `bsp_uart_host` | `uart_host/` | [uart_host/README.md](uart_host/README.md) |
|
||||
| `bsp_opto` | `opto/` | [opto/README.md](opto/README.md) |
|
||||
| `bsp_usb_cdc` | `usb_cdc/` | [usb_cdc/README.md](usb_cdc/README.md) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -107,32 +95,31 @@ target_link_libraries(bootloader PRIVATE
|
|||
|
||||
### Граница изоляции
|
||||
|
||||
Публичные заголовки компонента (`include/bsp/*.h`) не должны содержать ни одного `#include` из NXP SDK. Снаружи BSP — только стандартные типы C (`stdint.h`, `stdbool.h`, `stddef.h`) и собственные типы проекта.
|
||||
Публичные заголовки (`include/bsp/*.h`) не должны содержать ни одного `#include` из NXP SDK. Снаружи BSP — только стандартные типы C и собственные типы проекта.
|
||||
|
||||
```c
|
||||
/* ПРАВИЛЬНО — bsp/usb_cdc/include/bsp/usb_cdc.h */
|
||||
/* ПРАВИЛЬНО — bsp/opto/include/bsp/opto.h */
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
typedef enum { USB_CDC_OK, USB_CDC_ERR_NOT_READY } usb_cdc_status_t;
|
||||
usb_cdc_status_t usb_cdc_init(void);
|
||||
#include "bsp/status.h"
|
||||
|
||||
/* НЕПРАВИЛЬНО */
|
||||
#include "fsl_common.h" /* ← утечка NXP SDK наружу */
|
||||
#include "fsl_gpio.h" /* ← утечка NXP SDK наружу */
|
||||
```
|
||||
|
||||
Платформенные хедеры (`fsl_*.h`, `usb_device_*.h`) живут только в `src/` — как PRIVATE зависимости.
|
||||
Платформенные хедеры (`fsl_*.h`) живут только в `src/` — как PRIVATE зависимости.
|
||||
|
||||
### Структура одного компонента
|
||||
|
||||
```bash
|
||||
bsp/<name>/
|
||||
├── CMakeLists.txt
|
||||
├── README.md
|
||||
├── include/
|
||||
│ └── bsp/
|
||||
│ └── <name>.h # публичный API — без NXP хедеров
|
||||
└── src/
|
||||
├── <name>.c # реализация
|
||||
└── <конфиг>.h # приватные конфиги стека (напр. usb_device_config.h)
|
||||
└── <name>.c # реализация — fsl_*.h только здесь
|
||||
```
|
||||
|
||||
```cmake
|
||||
|
|
@ -140,21 +127,19 @@ bsp/<name>/
|
|||
add_library(bsp_<name> STATIC src/<name>.c)
|
||||
|
||||
target_include_directories(bsp_<name>
|
||||
PUBLIC include/ # bsp/<name>.h доступен снаружи
|
||||
PRIVATE src/ # конфиги и NXP хедеры — только внутри
|
||||
PUBLIC include/
|
||||
PRIVATE src/
|
||||
)
|
||||
|
||||
target_link_libraries(bsp_<name>
|
||||
PUBLIC bsp_board # транзитивно во все потребители
|
||||
PRIVATE sdk_<driver> # NXP SDK — не торчит наружу
|
||||
PUBLIC bsp_status
|
||||
PRIVATE bsp_board sdk_<driver>
|
||||
)
|
||||
```
|
||||
|
||||
### Защита от host-сборки
|
||||
|
||||
Каждый компонент должен быть безопасен при `BUILD_TESTS_HOST=ON`. Вариантов два:
|
||||
|
||||
**А — guard в CMakeLists (рекомендуется для большинства компонентов):**
|
||||
Компоненты с зависимостью от железа закрываются guard-ом в CMakeLists:
|
||||
|
||||
```cmake
|
||||
if(BUILD_TESTS_HOST)
|
||||
|
|
@ -162,42 +147,32 @@ if(BUILD_TESTS_HOST)
|
|||
endif()
|
||||
```
|
||||
|
||||
**Б — stub-реализация для компонентов которые тестируются на хосте:**
|
||||
|
||||
```c
|
||||
/* src/usb_cdc.c */
|
||||
#ifdef BSP_USB_CDC_VIRTUAL
|
||||
/* заглушка — пишет в stdout, используется в host-тестах */
|
||||
usb_cdc_status_t usb_cdc_write(const uint8_t *data, size_t len) {
|
||||
fwrite(data, 1, len, stdout);
|
||||
return USB_CDC_OK;
|
||||
}
|
||||
#else
|
||||
/* реальная реализация через NXP USB stack */
|
||||
#endif
|
||||
```
|
||||
Компоненты которые тестируются на хосте предоставляют fff-заглушки
|
||||
в `mocks/` (пример — `uart_host/mocks/`).
|
||||
|
||||
---
|
||||
|
||||
## Связь с generated/
|
||||
## generated/ — MCUXpresso Config Tools
|
||||
|
||||
`generated/` — выхлоп MCUXpresso Config Tools. Содержит конфигурацию тактирования, пинов и периферии для конкретной платы.
|
||||
`generated/` — выхлоп Config Tools. Содержит конфигурацию тактирования,
|
||||
пинов и периферии для конкретной платы. Источник истины — `TFT_Board.mex`.
|
||||
|
||||
**Что трогать можно:** файлы в `generated/` можно и нужно перегенерировать через Config Tools при изменении схемы.
|
||||
**Трогать нельзя:** редактировать файлы из `generated/` руками — изменения
|
||||
потеряются при следующей перегенерации.
|
||||
|
||||
**Что трогать нельзя:** редактировать `generated/` руками — изменения потеряются при следующей перегенерации.
|
||||
|
||||
**Как добавить новый пин или периферию:** открыть проект в MCUXpresso Config Tools → внести изменения → Update Code → закоммитить изменённые файлы из `generated/`.
|
||||
**Как добавить новый пин или периферию:** открыть `TFT_Board.mex` в
|
||||
MCUXpresso Config Tools → внести изменения → Update Code → закоммитить
|
||||
изменённые файлы из `generated/`.
|
||||
|
||||
---
|
||||
|
||||
## Добавление нового компонента — чеклист
|
||||
|
||||
```bash
|
||||
[ ] Создать bsp/<name>/ со структурой include/src/CMakeLists.txt
|
||||
[ ] Публичный хедер include/bsp/<name>.h — без NXP хедеров
|
||||
[ ] target_link_libraries: PUBLIC bsp_board, PRIVATE sdk_*
|
||||
[ ] Guard BUILD_TESTS_HOST в CMakeLists или stub-реализация в .c
|
||||
[ ] add_subdirectory(bsp/<name>) в bsp/CMakeLists.txt
|
||||
[ ] Добавить target в нужные прошивки (firmware/*/CMakeLists.txt)
|
||||
[ ] bsp/<name>/include/bsp/<name>.h — публичный API без NXP хедеров
|
||||
[ ] bsp/<name>/src/<name>.c — реализация
|
||||
[ ] bsp/<name>/CMakeLists.txt — guard BUILD_TESTS_HOST + зависимости
|
||||
[ ] bsp/<name>/README.md — аппаратура + API + использование
|
||||
[ ] bsp/CMakeLists.txt — add_subdirectory(<name>)
|
||||
[ ] firmware/*/CMakeLists.txt — добавить bsp_<name> в нужные прошивки
|
||||
```
|
||||
|
|
|
|||
|
|
@ -8,28 +8,64 @@
|
|||
| `BSP_OPTO_CH_IN2` | GPIO_AD_B1_05 | GPIO1[21] | PS2801-4 + 1K pull-up |
|
||||
| `BSP_OPTO_CH_RS` | GPIO_AD_B1_07 | GPIO1[23] | PS2801-4 + 1K pull-up |
|
||||
|
||||
**Логика:** active-low. Пин HIGH = нет тока = `INACTIVE`. Пин LOW = ток течёт = `ACTIVE`.
|
||||
**Логика:** active-HIGH. Оптопары неинвертирующие (PS2801-4):
|
||||
|
||||
- Пин HIGH (ток есть) → `BSP_OPTO_STATE_ACTIVE`
|
||||
- Пин LOW (тока нет) → `BSP_OPTO_STATE_INACTIVE`
|
||||
|
||||
`BSP_OPTO_CH_RS` — опциональный. Активен только при `rs_as_gpio = true` в конфигурации.
|
||||
При `rs_as_gpio = false` пин остаётся под управлением `bsp_uart_rs` (LPUART3_RX).
|
||||
|
||||
Все три пина принадлежат GPIO1[16..31] → один IRQ: `GPIO1_Combined_16_31_IRQn`.
|
||||
|
||||
## Дебаунс
|
||||
---
|
||||
|
||||
Аппаратного фильтра нет. Реализован программный дебаунс:
|
||||
## Режимы работы каналов
|
||||
|
||||
1. ISR фиксирует timestamp (`bsp_tick_get_ms()`) и raw состояние пина, выставляет `pending`.
|
||||
2. `bsp_opto_process()` вызывается из main loop. Если с момента последнего фронта прошло
|
||||
>= `debounce_ms` — перечитывает пин, сравнивает с `confirmed_state`, стреляет коллбэком.
|
||||
Каждый канал настраивается независимо через поле `modes[]` конфигурации.
|
||||
|
||||
### `BSP_OPTO_MODE_LEVEL` — детектирование уровня (IN1, IN2)
|
||||
|
||||
Предназначен для детектирования наличия/отсутствия сигнала с программным дебаунсом.
|
||||
|
||||
1. ISR фиксирует timestamp (`bsp_tick_get_ms()`) и raw состояние пина, взводит `pending`.
|
||||
2. ISR **автоматически переключает направление прерывания** (RISING↔FALLING) после каждого
|
||||
фронта — оба края сигнала ловятся без дополнительной настройки.
|
||||
3. `bsp_opto_process()` вызывается из main loop. Если с момента последнего фронта прошло
|
||||
>= `debounce_ms` — перечитывает пин, сравнивает с `confirmed_state`, вызывает коллбэк.
|
||||
|
||||
Начальный фронт выбирается **автоматически** при инициализации по текущему состоянию пина
|
||||
(LOW → ждём RISING, HIGH → ждём FALLING). Поле `edges[]` для этого режима игнорируется.
|
||||
|
||||
Рекомендуемое значение `debounce_ms`: **10 мс** (PS2801-4 response ~50 мкс,
|
||||
основной источник шума — механические контакты на стенде).
|
||||
|
||||
Коллбэк вызывается из контекста **main loop** (не из ISR).
|
||||
|
||||
### `BSP_OPTO_MODE_PROTO` — детектирование старт-бита протокола (RS)
|
||||
|
||||
Предназначен для приёма бинарных протоколов, где требуется минимальная задержка реакции
|
||||
на первый фронт (старт-бит).
|
||||
|
||||
1. ISR фиксирует фронт и **немедленно вызывает коллбэк** — без дебаунса.
|
||||
2. После срабатывания прерывание канала **отключается** автоматически.
|
||||
3. Принимающий модуль (декодер протокола) после обработки пакета вызывает
|
||||
`bsp_opto_proto_arm()` чтобы взвести прерывание для следующего старт-бита.
|
||||
|
||||
Направление фронта задаётся полем `edges[]` и не меняется автоматически
|
||||
(обычно `BSP_OPTO_EDGE_RISING` для старт-бита).
|
||||
|
||||
Коллбэк вызывается **прямо из ISR** — он должен быть ISR-safe:
|
||||
только взводить флаг или писать в `volatile`-переменную, никакой бизнес-логики.
|
||||
|
||||
---
|
||||
|
||||
## Использование
|
||||
|
||||
### MODE_LEVEL (IN1, IN2)
|
||||
|
||||
```c
|
||||
static void on_opto_change(bsp_opto_ch_t ch, bsp_opto_state_t state)
|
||||
static void on_level_change(bsp_opto_ch_t ch, bsp_opto_state_t state)
|
||||
{
|
||||
if (ch == BSP_OPTO_CH_IN1 && state == BSP_OPTO_STATE_ACTIVE) {
|
||||
/* IN1 активирован */
|
||||
|
|
@ -37,7 +73,9 @@ static void on_opto_change(bsp_opto_ch_t ch, bsp_opto_state_t state)
|
|||
}
|
||||
|
||||
bsp_opto_config_t cfg = {
|
||||
.callbacks = { on_opto_change, on_opto_change, NULL },
|
||||
.callbacks = { on_level_change, on_level_change, NULL },
|
||||
.modes = { BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_LEVEL },
|
||||
.edges = { BSP_OPTO_EDGE_RISING, BSP_OPTO_EDGE_RISING, BSP_OPTO_EDGE_RISING },
|
||||
.rs_as_gpio = false,
|
||||
.debounce_ms = 10U,
|
||||
};
|
||||
|
|
@ -46,7 +84,6 @@ bsp_opto_init(&cfg);
|
|||
/* в main loop: */
|
||||
for (;;) {
|
||||
bsp_opto_process();
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -56,6 +93,44 @@ for (;;) {
|
|||
bsp_opto_state_t state = bsp_opto_read(BSP_OPTO_CH_IN1);
|
||||
```
|
||||
|
||||
### MODE_PROTO (RS) совместно с MODE_LEVEL (IN1, IN2)
|
||||
|
||||
```c
|
||||
/* Коллбэк вызывается из ISR — только атомарные операции */
|
||||
static volatile bool s_start_bit_detected = false;
|
||||
|
||||
static void on_rs_start_bit(bsp_opto_ch_t ch, bsp_opto_state_t state)
|
||||
{
|
||||
s_start_bit_detected = true;
|
||||
}
|
||||
|
||||
static void on_level_change(bsp_opto_ch_t ch, bsp_opto_state_t state) { /* ... */ }
|
||||
|
||||
bsp_opto_config_t cfg = {
|
||||
.callbacks = { on_level_change, on_level_change, on_rs_start_bit },
|
||||
.modes = { BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_PROTO },
|
||||
.edges = { BSP_OPTO_EDGE_RISING, BSP_OPTO_EDGE_RISING, BSP_OPTO_EDGE_RISING },
|
||||
.rs_as_gpio = true,
|
||||
.debounce_ms = 10U,
|
||||
};
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
/* в main loop: */
|
||||
for (;;) {
|
||||
bsp_opto_process(); /* обслуживает IN1, IN2 */
|
||||
|
||||
if (s_start_bit_detected) {
|
||||
s_start_bit_detected = false;
|
||||
/* запустить декодер протокола по таймеру... */
|
||||
|
||||
/* по завершении приёма пакета — взвести для следующего старт-бита */
|
||||
bsp_opto_proto_arm(BSP_OPTO_CH_RS);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Совместное использование RS_RX
|
||||
|
||||
Пин GPIO_AD_B1_07 может работать в двух режимах:
|
||||
|
|
|
|||
|
|
@ -2,25 +2,55 @@
|
|||
* bsp_opto — оптоизолированные входы
|
||||
*
|
||||
* Аппаратура:
|
||||
* EXT_IN1 GPIO1[22] GPIO_AD_B1_06 active-low (PS2801-4, pull-up 1K к 3V3)
|
||||
* EXT_IN2 GPIO1[21] GPIO_AD_B1_05 active-low (PS2801-4, pull-up 1K к 3V3)
|
||||
* RS_RX GPIO1[23] GPIO_AD_B1_07 active-low (PS2801-4, pull-up 1K к 3V3)
|
||||
* EXT_IN1 GPIO1[22] GPIO_AD_B1_06 active-high (PS2801-4, неинвертирующая)
|
||||
* EXT_IN2 GPIO1[21] GPIO_AD_B1_05 active-high (PS2801-4, неинвертирующая)
|
||||
* RS_RX GPIO1[23] GPIO_AD_B1_07 active-high (PS2801-4, неинвертирующая)
|
||||
* опциональный канал — только при rs_as_gpio == true
|
||||
*
|
||||
* Дебаунс: программный, на базе bsp_tick.
|
||||
* ISR фиксирует timestamp + raw state → bsp_opto_process() из main loop
|
||||
* подтверждает стабильное состояние и стреляет коллбэком.
|
||||
* Логика: active-HIGH. Оптопара неинвертирующая:
|
||||
* Пин HIGH (ток есть) → BSP_OPTO_STATE_ACTIVE
|
||||
* Пин LOW (тока нет) → BSP_OPTO_STATE_INACTIVE
|
||||
*
|
||||
* Режимы каналов (bsp_opto_ch_mode_t):
|
||||
* ы
|
||||
* BSP_OPTO_MODE_LEVEL — IN1, IN2
|
||||
* Детектирование уровня с программным дебаунсом.
|
||||
* ISR переключает направление прерывания (RISING↔FALLING) после каждого фронта,
|
||||
* чтобы ловить оба края. bsp_opto_process() вызывается из main loop и подтверждает
|
||||
* стабильное состояние после истечения debounce_ms.
|
||||
* Коллбэк вызывается из контекста main loop (НЕ из ISR).
|
||||
*
|
||||
* BSP_OPTO_MODE_PROTO — RS
|
||||
* Детектирование одиночного фронта без дебаунса для приёма бинарного протокола.
|
||||
* Коллбэк вызывается ПРЯМО ИЗ ISR — минимальная задержка.
|
||||
* Коллбэк должен быть ISR-safe: только взводить флаг / писать в volatile-переменную.
|
||||
* После каждого вызова прерывание отключается — принимающий модуль
|
||||
* должен вызвать bsp_opto_proto_arm() для подготовки к следующему старт-биту.
|
||||
*
|
||||
* Использование (MODE_LEVEL):
|
||||
* static void on_level_change(bsp_opto_ch_t ch, bsp_opto_state_t state) { ... }
|
||||
*
|
||||
* Использование:
|
||||
* bsp_opto_config_t cfg = {
|
||||
* .callbacks = { my_cb, my_cb, NULL },
|
||||
* .rs_as_gpio = false,
|
||||
* .callbacks = { on_level_change, on_level_change, NULL },
|
||||
* .modes = { BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_PROTO },
|
||||
* .rs_as_gpio = true,
|
||||
* .debounce_ms = 10U,
|
||||
* };
|
||||
* bsp_opto_init(&cfg);
|
||||
*
|
||||
* // в main loop:
|
||||
* bsp_opto_process();
|
||||
*
|
||||
* Использование (MODE_PROTO для RS):
|
||||
* static volatile bool s_start_bit = false;
|
||||
*
|
||||
* static void on_rs_start(bsp_opto_ch_t ch, bsp_opto_state_t state)
|
||||
* {
|
||||
* s_start_bit = true; // только это — вызывается из ISR
|
||||
* }
|
||||
*
|
||||
* // После обработки старт-бита — взвести снова:
|
||||
* bsp_opto_proto_arm(BSP_OPTO_CH_RS);
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
|
@ -44,10 +74,32 @@ typedef enum
|
|||
|
||||
typedef enum
|
||||
{
|
||||
BSP_OPTO_STATE_INACTIVE = 0U, /* цепь разомкнута, тока нет */
|
||||
BSP_OPTO_STATE_ACTIVE, /* цепь замкнута, ток течёт */
|
||||
BSP_OPTO_STATE_INACTIVE = 0U, /* тока нет, пин LOW */
|
||||
BSP_OPTO_STATE_ACTIVE, /* ток есть, пин HIGH */
|
||||
} bsp_opto_state_t;
|
||||
|
||||
/* ── Режим работы канала ─────────────────────────────────────────────────── */
|
||||
|
||||
typedef enum
|
||||
{
|
||||
/**
|
||||
* Детектирование уровня с дебаунсом.
|
||||
* Подходит для IN1, IN2.
|
||||
* ISR автоматически переключает направление (RISING↔FALLING).
|
||||
* Коллбэк вызывается из bsp_opto_process() — контекст main loop.
|
||||
*/
|
||||
BSP_OPTO_MODE_LEVEL = 0U,
|
||||
|
||||
/**
|
||||
* Детектирование одиночного фронта без дебаунса.
|
||||
* Подходит для RS при приёме бинарного протокола.
|
||||
* Коллбэк вызывается ПРЯМО ИЗ ISR.
|
||||
* После срабатывания прерывание отключается до вызова bsp_opto_proto_arm().
|
||||
* edges[ch] задаёт фронт начального старт-бита (обычно RISING).
|
||||
*/
|
||||
BSP_OPTO_MODE_PROTO,
|
||||
} bsp_opto_ch_mode_t;
|
||||
|
||||
/* ── Фронт прерывания ────────────────────────────────────────────────────── */
|
||||
|
||||
typedef enum
|
||||
|
|
@ -61,9 +113,11 @@ typedef enum
|
|||
/**
|
||||
* @brief Коллбэк изменения состояния канала.
|
||||
*
|
||||
* Вызывается из bsp_opto_process() (контекст main loop, не ISR).
|
||||
* Для MODE_LEVEL: вызывается из bsp_opto_process() — контекст main loop.
|
||||
* Для MODE_PROTO: вызывается из ISR — только атомарные операции!
|
||||
*
|
||||
* @param ch канал, изменивший состояние
|
||||
* @param state новое подтверждённое (дебаунсированное) состояние
|
||||
* @param state новое состояние пина в момент фронта
|
||||
*/
|
||||
typedef void (*bsp_opto_callback_t)(bsp_opto_ch_t input_channel, bsp_opto_state_t state);
|
||||
|
||||
|
|
@ -78,21 +132,32 @@ typedef struct
|
|||
bsp_opto_callback_t callbacks[BSP_OPTO_CH_COUNT];
|
||||
|
||||
/**
|
||||
* Фронт срабатывания прерывания
|
||||
*
|
||||
* Режим работы каждого канала.
|
||||
* MODE_LEVEL — дебаунс + коллбэк из main loop.
|
||||
* MODE_PROTO — без дебаунса, коллбэк из ISR.
|
||||
*/
|
||||
bsp_opto_ch_mode_t modes[BSP_OPTO_CH_COUNT];
|
||||
|
||||
/**
|
||||
* Начальный фронт прерывания.
|
||||
* MODE_LEVEL: игнорируется — ISR сам переключает направление.
|
||||
* Начальный фронт выбирается автоматически по текущему
|
||||
* состоянию пина при инициализации.
|
||||
* MODE_PROTO: фронт старт-бита (обычно BSP_OPTO_EDGE_RISING).
|
||||
*/
|
||||
bsp_opto_edge_t edges[BSP_OPTO_CH_COUNT];
|
||||
|
||||
/**
|
||||
* true — RS_RX сконфигурировать как GPIO (BSP_OPTO_CH_RS активен).
|
||||
* false — RS_RX остаётся в состоянии по умолчанию (LPUART3 / AIN).
|
||||
* false — RS_RX остаётся под управлением LPUART3.
|
||||
* BSP_OPTO_CH_RS недоступен, его коллбэк игнорируется.
|
||||
*/
|
||||
bool rs_as_gpio;
|
||||
|
||||
/**
|
||||
* Период дебаунса в миллисекундах. Рекомендуется 5–10 мс.
|
||||
* 0 — дебаунс отключён (для тестирования).
|
||||
* Период дебаунса в миллисекундах для MODE_LEVEL.
|
||||
* Рекомендуется 5–10 мс. 0 — дебаунс отключён (для тестирования).
|
||||
* Для MODE_PROTO не используется.
|
||||
*/
|
||||
uint32_t debounce_ms;
|
||||
} bsp_opto_config_t;
|
||||
|
|
@ -107,20 +172,20 @@ extern "C"
|
|||
/**
|
||||
* @brief Инициализировать модуль.
|
||||
*
|
||||
* Конфигурирует пины (pin mux), настраивает GPIO на вход,
|
||||
* включает прерывания по обоим фронтам. Для RS_RX вызывает
|
||||
* BOARD_InitRS_GPIO() если rs_as_gpio == true.
|
||||
* Конфигурирует пины, настраивает GPIO на вход, включает прерывания.
|
||||
* Для RS_RX вызывает BOARD_InitRS_GPIO() если rs_as_gpio == true.
|
||||
* Начальное состояние каналов MODE_LEVEL читается с пинов при инициализации.
|
||||
*
|
||||
* @param p_config указатель на конфигурацию (не NULL)
|
||||
* @return BSP_OK или BSP_ERR_INVALID_ARG
|
||||
* @return BSP_OK или BSP_ERR_PARAM
|
||||
*/
|
||||
bsp_status_t bsp_opto_init(const bsp_opto_config_t *p_config);
|
||||
|
||||
/**
|
||||
* @brief Прочитать текущее подтверждённое состояние канала.
|
||||
* @brief Прочитать текущее подтверждённое состояние канала (только MODE_LEVEL).
|
||||
*
|
||||
* Возвращает последнее состояние, зафиксированное после дебаунса.
|
||||
* Может вызываться из любого контекста.
|
||||
* Для MODE_PROTO всегда возвращает BSP_OPTO_STATE_INACTIVE —
|
||||
* состояние RS отслеживается коллбэком из ISR.
|
||||
*
|
||||
* @param ch номер канала
|
||||
* @return BSP_OPTO_STATE_INACTIVE / BSP_OPTO_STATE_ACTIVE
|
||||
|
|
@ -128,18 +193,26 @@ extern "C"
|
|||
bsp_opto_state_t bsp_opto_read(bsp_opto_ch_t input_channel);
|
||||
|
||||
/**
|
||||
* @brief Обработать отложенные события дебаунса.
|
||||
* @brief Обработать отложенные события дебаунса (только MODE_LEVEL).
|
||||
*
|
||||
* Должна вызываться из main loop на каждой итерации.
|
||||
* НЕ вызывать из ISR.
|
||||
*
|
||||
* Для каждого канала с pending-флагом:
|
||||
* — если прошло >= debounce_ms с последнего фронта,
|
||||
* перечитывает пин, сравнивает с confirmed_state,
|
||||
* при изменении вызывает коллбэк и обновляет confirmed_state.
|
||||
* Должна вызываться из main loop на каждой итерации. НЕ вызывать из ISR.
|
||||
* Каналы MODE_PROTO пропускаются — для них используется коллбэк из ISR.
|
||||
*/
|
||||
void bsp_opto_process(void);
|
||||
|
||||
/**
|
||||
* @brief Взвести прерывание канала MODE_PROTO для приёма следующего старт-бита.
|
||||
*
|
||||
* После того как ISR сработал и вызвал коллбэк, прерывание канала
|
||||
* отключается. Принимающий модуль должен вызвать эту функцию когда
|
||||
* готов принять следующий пакет.
|
||||
*
|
||||
* Безопасно вызывать из main loop. Для каналов MODE_LEVEL — no-op.
|
||||
*
|
||||
* @param ch канал (обычно BSP_OPTO_CH_RS)
|
||||
*/
|
||||
void bsp_opto_proto_arm(bsp_opto_ch_t ch);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,5 +1,17 @@
|
|||
/*
|
||||
* bsp_opto — реализация
|
||||
*
|
||||
* Изменения относительно предыдущей версии:
|
||||
*
|
||||
* 1. Полярность: оптопары неинвертирующие → ACTIVE = HIGH = 1.
|
||||
*
|
||||
* 2. MODE_LEVEL (IN1, IN2): ISR переключает направление прерывания
|
||||
* после каждого фронта (RISING↔FALLING), поэтому ловятся оба края.
|
||||
* bsp_opto_process() обрабатывает только каналы с pending == true.
|
||||
*
|
||||
* 3. MODE_PROTO (RS): коллбэк вызывается прямо из ISR без дебаунса.
|
||||
* После срабатывания прерывание канала отключается.
|
||||
* bsp_opto_proto_arm() взводит его снова когда декодер готов.
|
||||
*/
|
||||
|
||||
#include "bsp/opto.h"
|
||||
|
|
@ -23,8 +35,7 @@
|
|||
#define OPTO_IN2_PIN 21U
|
||||
#define OPTO_RS_PIN 23U
|
||||
|
||||
//FIXME: fixed (оптопара неинтвертирующая: когда на входе 1, на выходе тоже будет 1)
|
||||
/* Active-HIGH: оптопара тянет пин к VCC → ACTIVE = HIGH = 1 */
|
||||
/* Active-HIGH: оптопара неинвертирующая → ACTIVE = пин HIGH = raw 1 */
|
||||
#define OPTO_PIN_TO_STATE(raw) ((raw) == 1U ? BSP_OPTO_STATE_ACTIVE : BSP_OPTO_STATE_INACTIVE)
|
||||
|
||||
/* ── Состояние канала ────────────────────────────────────────────────────── */
|
||||
|
|
@ -36,6 +47,8 @@ typedef struct
|
|||
volatile uint32_t raw_pin; /* raw состояние пина в момент фронта */
|
||||
bsp_opto_state_t confirmed_state; /* последнее подтверждённое состояние */
|
||||
uint8_t pin; /* номер пина в GPIO1 */
|
||||
bsp_opto_ch_mode_t mode; /* режим канала */
|
||||
bsp_opto_edge_t next_edge; /* следующий ожидаемый фронт (LEVEL) */
|
||||
bool enabled; /* канал активен */
|
||||
} opto_ch_state_t;
|
||||
|
||||
|
|
@ -49,7 +62,7 @@ typedef struct
|
|||
bool initialised;
|
||||
} opto_module_t;
|
||||
|
||||
static opto_module_t g_s_opto;
|
||||
static opto_module_t s_opto;
|
||||
|
||||
/* ── Вспомогательные функции ─────────────────────────────────────────────── */
|
||||
|
||||
|
|
@ -58,11 +71,23 @@ static uint32_t read_pin(uint8_t pin)
|
|||
return GPIO_PinRead(OPTO_GPIO_BASE, pin);
|
||||
}
|
||||
|
||||
static void enable_irq(uint8_t pin, bsp_opto_edge_t edge)
|
||||
static gpio_interrupt_mode_t edge_to_irq_mode(bsp_opto_edge_t edge)
|
||||
{
|
||||
gpio_interrupt_mode_t mode =
|
||||
(edge == BSP_OPTO_EDGE_RISING) ? kGPIO_IntRisingEdge : kGPIO_IntFallingEdge;
|
||||
GPIO_SetPinInterruptConfig(OPTO_GPIO_BASE, pin, mode);
|
||||
return (edge == BSP_OPTO_EDGE_RISING) ? kGPIO_IntRisingEdge : kGPIO_IntFallingEdge;
|
||||
}
|
||||
|
||||
static bsp_opto_edge_t opposite_edge(bsp_opto_edge_t edge)
|
||||
{
|
||||
return (edge == BSP_OPTO_EDGE_RISING) ? BSP_OPTO_EDGE_FALLING : BSP_OPTO_EDGE_RISING;
|
||||
}
|
||||
|
||||
/*
|
||||
* Настроить и включить прерывание на заданный фронт.
|
||||
* Используется как при инициализации, так и при взводе proto_arm().
|
||||
*/
|
||||
static void set_irq_edge(uint8_t pin, bsp_opto_edge_t edge)
|
||||
{
|
||||
GPIO_SetPinInterruptConfig(OPTO_GPIO_BASE, pin, edge_to_irq_mode(edge));
|
||||
GPIO_EnableInterrupts(OPTO_GPIO_BASE, 1UL << pin);
|
||||
}
|
||||
|
||||
|
|
@ -71,15 +96,24 @@ static void enable_irq(uint8_t pin, bsp_opto_edge_t edge)
|
|||
/*
|
||||
* Общий обработчик для GPIO1[16..31].
|
||||
* Все три канала (пины 21, 22, 23) попадают сюда.
|
||||
*
|
||||
* MODE_LEVEL:
|
||||
* — Фиксируем raw + timestamp, взводим pending.
|
||||
* — Переключаем направление прерывания на противоположный фронт,
|
||||
* чтобы поймать возврат сигнала.
|
||||
*
|
||||
* MODE_PROTO:
|
||||
* — Сразу вызываем коллбэк (ISR-контекст, коллбэк должен быть ISR-safe).
|
||||
* — Отключаем прерывание канала — декодер взведёт его снова через
|
||||
* bsp_opto_proto_arm() когда будет готов принять следующий пакет.
|
||||
*/
|
||||
|
||||
void GPIO1_Combined_16_31_IRQHandler(void) // NOLINT(readability-identifier-naming)
|
||||
{
|
||||
uint32_t flags = GPIO_GetPinsInterruptFlags(OPTO_GPIO_BASE);
|
||||
|
||||
for (bsp_opto_ch_t ch = 0U; ch < BSP_OPTO_CH_COUNT; ch++)
|
||||
for (bsp_opto_ch_t ch = (bsp_opto_ch_t) 0U; ch < BSP_OPTO_CH_COUNT; ch++)
|
||||
{
|
||||
opto_ch_state_t *p_ch = &g_s_opto.channels[ch];
|
||||
opto_ch_state_t *p_ch = &s_opto.channels[ch];
|
||||
|
||||
if (!p_ch->enabled)
|
||||
{
|
||||
|
|
@ -87,13 +121,36 @@ void GPIO1_Combined_16_31_IRQHandler(void) // NOLINT(readability-identifier-nami
|
|||
}
|
||||
|
||||
uint32_t mask = 1UL << p_ch->pin;
|
||||
if ((flags & mask) != 0U)
|
||||
if ((flags & mask) == 0U)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Сбрасываем флаг прерывания сразу */
|
||||
GPIO_ClearPinsInterruptFlags(OPTO_GPIO_BASE, mask);
|
||||
|
||||
p_ch->raw_pin = read_pin(p_ch->pin);
|
||||
|
||||
if (p_ch->mode == BSP_OPTO_MODE_PROTO)
|
||||
{
|
||||
/* Отключить прерывание — декодер взведёт его через proto_arm() */
|
||||
GPIO_DisableInterrupts(OPTO_GPIO_BASE, mask);
|
||||
|
||||
/* Вызвать коллбэк прямо из ISR */
|
||||
if (s_opto.callbacks[ch] != NULL)
|
||||
{
|
||||
s_opto.callbacks[ch](ch, OPTO_PIN_TO_STATE(p_ch->raw_pin));
|
||||
}
|
||||
}
|
||||
else /* BSP_OPTO_MODE_LEVEL */
|
||||
{
|
||||
p_ch->last_edge_ms = bsp_tick_get_ms();
|
||||
p_ch->pending = true;
|
||||
|
||||
GPIO_ClearPinsInterruptFlags(OPTO_GPIO_BASE, mask);
|
||||
/* Переключить направление: следующий фронт — противоположный */
|
||||
p_ch->next_edge = opposite_edge(p_ch->next_edge);
|
||||
GPIO_SetPinInterruptConfig(OPTO_GPIO_BASE, p_ch->pin,
|
||||
edge_to_irq_mode(p_ch->next_edge));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,13 +167,12 @@ bsp_status_t bsp_opto_init(const bsp_opto_config_t *p_config)
|
|||
}
|
||||
|
||||
/* Сохранить конфигурацию */
|
||||
for (bsp_opto_ch_t ch = 0U; ch < BSP_OPTO_CH_COUNT; ch++)
|
||||
for (bsp_opto_ch_t ch = (bsp_opto_ch_t) 0U; ch < BSP_OPTO_CH_COUNT; ch++)
|
||||
{
|
||||
g_s_opto.callbacks[ch] = p_config->callbacks[ch];
|
||||
s_opto.callbacks[ch] = p_config->callbacks[ch];
|
||||
}
|
||||
g_s_opto.debounce_ms = p_config->debounce_ms;
|
||||
s_opto.debounce_ms = p_config->debounce_ms;
|
||||
|
||||
/* Настройка каналов IN1, IN2 — всегда активны */
|
||||
static const uint8_t K_PINS[BSP_OPTO_CH_COUNT] = {
|
||||
[BSP_OPTO_CH_IN1] = OPTO_IN1_PIN,
|
||||
[BSP_OPTO_CH_IN2] = OPTO_IN2_PIN,
|
||||
|
|
@ -129,9 +185,9 @@ bsp_status_t bsp_opto_init(const bsp_opto_config_t *p_config)
|
|||
.interruptMode = kGPIO_NoIntmode,
|
||||
};
|
||||
|
||||
for (bsp_opto_ch_t ch = 0U; ch < BSP_OPTO_CH_COUNT; ch++)
|
||||
for (bsp_opto_ch_t ch = (bsp_opto_ch_t) 0U; ch < BSP_OPTO_CH_COUNT; ch++)
|
||||
{
|
||||
opto_ch_state_t *p_ch = &g_s_opto.channels[ch];
|
||||
opto_ch_state_t *p_ch = &s_opto.channels[ch];
|
||||
|
||||
if (ch == BSP_OPTO_CH_RS && !p_config->rs_as_gpio)
|
||||
{
|
||||
|
|
@ -140,25 +196,46 @@ bsp_status_t bsp_opto_init(const bsp_opto_config_t *p_config)
|
|||
}
|
||||
|
||||
p_ch->pin = K_PINS[ch];
|
||||
p_ch->mode = p_config->modes[ch];
|
||||
p_ch->enabled = true;
|
||||
p_ch->pending = false;
|
||||
|
||||
/* Начальное состояние — читаем пин до включения прерываний */
|
||||
if (ch == BSP_OPTO_CH_RS)
|
||||
{
|
||||
BOARD_InitRS_GPIO();
|
||||
}
|
||||
|
||||
GPIO_PinInit(OPTO_GPIO_BASE, p_ch->pin, &gpio_in_cfg);
|
||||
p_ch->confirmed_state = OPTO_PIN_TO_STATE(read_pin(p_ch->pin));
|
||||
|
||||
enable_irq(p_ch->pin, p_config->edges[ch]);
|
||||
if (p_ch->mode == BSP_OPTO_MODE_LEVEL)
|
||||
{
|
||||
/*
|
||||
* Для MODE_LEVEL начальный фронт выбираем по текущему состоянию пина:
|
||||
* пин LOW → ждём RISING (сигнал ещё не пришёл)
|
||||
* пин HIGH → ждём FALLING (сигнал уже активен)
|
||||
* Это гарантирует, что мы не пропустим первое изменение.
|
||||
*/
|
||||
uint32_t current_raw = read_pin(p_ch->pin);
|
||||
p_ch->confirmed_state = OPTO_PIN_TO_STATE(current_raw);
|
||||
p_ch->next_edge = (current_raw == 0U) ? BSP_OPTO_EDGE_RISING : BSP_OPTO_EDGE_FALLING;
|
||||
set_irq_edge(p_ch->pin, p_ch->next_edge);
|
||||
}
|
||||
else /* BSP_OPTO_MODE_PROTO */
|
||||
{
|
||||
/*
|
||||
* Для MODE_PROTO используем фронт из конфигурации —
|
||||
* обычно RISING (старт-бит протокола).
|
||||
* confirmed_state не используется для этого режима.
|
||||
*/
|
||||
p_ch->confirmed_state = BSP_OPTO_STATE_INACTIVE;
|
||||
p_ch->next_edge = p_config->edges[ch];
|
||||
set_irq_edge(p_ch->pin, p_ch->next_edge);
|
||||
}
|
||||
}
|
||||
|
||||
/* Включить IRQ GPIO1_Combined_16_31 */
|
||||
EnableIRQ(GPIO1_Combined_16_31_IRQn);
|
||||
|
||||
g_s_opto.initialised = true;
|
||||
s_opto.initialised = true;
|
||||
return BSP_OK;
|
||||
}
|
||||
|
||||
|
|
@ -166,35 +243,58 @@ bsp_status_t bsp_opto_init(const bsp_opto_config_t *p_config)
|
|||
|
||||
bsp_opto_state_t bsp_opto_read(bsp_opto_ch_t input_channel)
|
||||
{
|
||||
if (input_channel >= BSP_OPTO_CH_COUNT || !g_s_opto.channels[input_channel].enabled)
|
||||
if (input_channel >= BSP_OPTO_CH_COUNT || !s_opto.channels[input_channel].enabled ||
|
||||
s_opto.channels[input_channel].mode == BSP_OPTO_MODE_PROTO)
|
||||
{
|
||||
return BSP_OPTO_STATE_INACTIVE;
|
||||
}
|
||||
return g_s_opto.channels[input_channel].confirmed_state;
|
||||
return s_opto.channels[input_channel].confirmed_state;
|
||||
}
|
||||
|
||||
/* ── Обработка дебаунса ──────────────────────────────────────────────────── */
|
||||
/* ── Взвод MODE_PROTO для следующего пакета ──────────────────────────────── */
|
||||
|
||||
void bsp_opto_proto_arm(bsp_opto_ch_t ch)
|
||||
{
|
||||
if (ch >= BSP_OPTO_CH_COUNT)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
opto_ch_state_t *p_ch = &s_opto.channels[ch];
|
||||
|
||||
if (!p_ch->enabled || p_ch->mode != BSP_OPTO_MODE_PROTO)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* Сбросить старый флаг прерывания и взвести снова */
|
||||
GPIO_ClearPinsInterruptFlags(OPTO_GPIO_BASE, 1UL << p_ch->pin);
|
||||
set_irq_edge(p_ch->pin, p_ch->next_edge);
|
||||
}
|
||||
|
||||
/* ── Обработка дебаунса (только MODE_LEVEL) ──────────────────────────────── */
|
||||
|
||||
void bsp_opto_process(void)
|
||||
{
|
||||
if (!g_s_opto.initialised)
|
||||
if (!s_opto.initialised)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now = bsp_tick_get_ms();
|
||||
|
||||
for (bsp_opto_ch_t ch = 0U; ch < BSP_OPTO_CH_COUNT; ch++)
|
||||
for (bsp_opto_ch_t ch = (bsp_opto_ch_t) 0U; ch < BSP_OPTO_CH_COUNT; ch++)
|
||||
{
|
||||
opto_ch_state_t *p_ch = &g_s_opto.channels[ch];
|
||||
// TODO: разобраться как ловить ситуацию, когда сигнал отключается (Возможно нужно включить, как RISING, так и FALLING (кроме RS))
|
||||
// if (!p_ch->enabled || !p_ch->pending)
|
||||
// {
|
||||
// continue;
|
||||
// }
|
||||
opto_ch_state_t *p_ch = &s_opto.channels[ch];
|
||||
|
||||
/* Пропускаем: выключен, нет события, или proto-канал (обрабатывается в ISR) */
|
||||
if (!p_ch->enabled || !p_ch->pending || p_ch->mode == BSP_OPTO_MODE_PROTO)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t elapsed = now - p_ch->last_edge_ms;
|
||||
if (elapsed < g_s_opto.debounce_ms)
|
||||
if (elapsed < s_opto.debounce_ms)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -207,9 +307,9 @@ void bsp_opto_process(void)
|
|||
{
|
||||
p_ch->confirmed_state = new_state;
|
||||
|
||||
if (g_s_opto.callbacks[ch] != NULL)
|
||||
if (s_opto.callbacks[ch] != NULL)
|
||||
{
|
||||
g_s_opto.callbacks[ch](ch, new_state);
|
||||
s_opto.callbacks[ch](ch, new_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,219 +0,0 @@
|
|||
# PRIVATE, PUBLIC и INTERFACE в CMake
|
||||
|
||||
## Основная идея
|
||||
|
||||
Каждый таргет в CMake — это «чёрный ящик» с двумя границами:
|
||||
|
||||
- **внутренняя** — то, что нужно только для компиляции самого таргета
|
||||
- **внешняя** — то, что таргет «экспортирует» своим потребителям
|
||||
|
||||
Ключевые слова `PRIVATE`, `PUBLIC` и `INTERFACE` управляют тем, в какую из этих границ попадает свойство.
|
||||
|
||||
``````bash
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ lib_a │
|
||||
│ │
|
||||
│ PRIVATE │ INTERFACE │
|
||||
│ (только внутри) │ (только снаружи) │
|
||||
│ │ │
|
||||
│ PUBLIC = PRIVATE + INTERFACE │
|
||||
└─────────────────────────────────────────────────┘
|
||||
│ target_link_libraries(app PRIVATE lib_a)
|
||||
▼
|
||||
[ app ] ← получает только INTERFACE-свойства lib_a
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Определения
|
||||
|
||||
| Ключевое слово | Применяется к самому таргету | Передаётся потребителям |
|
||||
|----------------|:----------------------------:|:-----------------------:|
|
||||
| `PRIVATE` | ✅ | ❌ |
|
||||
| `PUBLIC` | ✅ | ✅ |
|
||||
| `INTERFACE` | ❌ | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Разбор на примерах
|
||||
|
||||
### Пример 1 — `target_include_directories`
|
||||
|
||||
``````bash
|
||||
src/
|
||||
├── lib_math/
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── include/ ← публичные заголовки (нужны потребителям)
|
||||
│ │ └── math.h
|
||||
│ ├── internal/ ← внутренние заголовки (только для lib_math)
|
||||
│ │ └── impl.h
|
||||
│ └── math.c
|
||||
└── app/
|
||||
├── CMakeLists.txt
|
||||
└── main.c ← #include "math.h"
|
||||
```
|
||||
|
||||
```cmake
|
||||
# lib_math/CMakeLists.txt
|
||||
add_library(lib_math STATIC math.c)
|
||||
|
||||
target_include_directories(lib_math
|
||||
PRIVATE internal/ # impl.h нужен только при компиляции math.c
|
||||
PUBLIC include/ # math.h нужен и lib_math, и всем её потребителям
|
||||
)
|
||||
```
|
||||
|
||||
```cmake
|
||||
# app/CMakeLists.txt
|
||||
add_library(app main.c)
|
||||
target_link_libraries(app PRIVATE lib_math)
|
||||
# app автоматически получает include/ через PUBLIC-свойство lib_math
|
||||
# app НЕ получает internal/ — оно PRIVATE
|
||||
```
|
||||
|
||||
**Итог:** `main.c` может писать `#include "math.h"`, но не видит `impl.h`.
|
||||
|
||||
---
|
||||
|
||||
### Пример 2 — `target_compile_definitions`
|
||||
|
||||
``````bash
|
||||
lib_json ──→ lib_http ──→ app
|
||||
```
|
||||
|
||||
```cmake
|
||||
# lib_json
|
||||
add_library(lib_json STATIC json.c)
|
||||
target_compile_definitions(lib_json
|
||||
PRIVATE JSON_INTERNAL_DEBUG # дефайн только для json.c
|
||||
PUBLIC JSON_VERSION=2 # нужен и json.c, и потребителям
|
||||
)
|
||||
```
|
||||
|
||||
```cmake
|
||||
# lib_http линкуется с lib_json
|
||||
add_library(lib_http STATIC http.c)
|
||||
target_link_libraries(lib_http PUBLIC lib_json)
|
||||
# lib_http транзитивно передаёт JSON_VERSION=2 дальше в app
|
||||
```
|
||||
|
||||
```cmake
|
||||
# app
|
||||
add_executable(app main.c)
|
||||
target_link_libraries(app PRIVATE lib_http)
|
||||
# app видит JSON_VERSION=2 (транзитивно через lib_http → lib_json)
|
||||
# app НЕ видит JSON_INTERNAL_DEBUG (PRIVATE)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Пример 3 — `INTERFACE` (header-only библиотека)
|
||||
|
||||
`INTERFACE` используется тогда, когда таргет сам **не компилируется** — например, header-only библиотека или набор флагов.
|
||||
|
||||
```cmake
|
||||
# Набор флагов для встроенных таргетов — сам не компилируется
|
||||
add_library(flags_embedded INTERFACE)
|
||||
|
||||
target_compile_options(flags_embedded INTERFACE
|
||||
-mcpu=cortex-m7
|
||||
-mfpu=fpv5-d16
|
||||
-mfloat-abi=hard
|
||||
-mthumb
|
||||
)
|
||||
|
||||
target_compile_definitions(flags_embedded INTERFACE
|
||||
ARM_MATH_CM7
|
||||
__FPU_PRESENT=1
|
||||
)
|
||||
```
|
||||
|
||||
```cmake
|
||||
# Любой firmware-таргет подключает флаги одной строкой
|
||||
target_link_libraries(firmware_app PRIVATE flags_embedded)
|
||||
# firmware_app получает все -mcpu, -mfpu и дефайны
|
||||
```
|
||||
|
||||
Такой паттерн часто используется для:
|
||||
|
||||
- HAL/SDK флагов конкретного МК
|
||||
- Флагов линкера (`-T linker.ld`)
|
||||
- Опций оптимизации для конкретного сценария сборки
|
||||
|
||||
---
|
||||
|
||||
### Пример 4 — транзитивность
|
||||
|
||||
Понять транзитивность важно: свойства распространяются по цепочке зависимостей.
|
||||
|
||||
```
|
||||
lib_base ──→ lib_mid ──→ app
|
||||
```
|
||||
|
||||
```cmake
|
||||
add_library(lib_base STATIC base.c)
|
||||
target_compile_definitions(lib_base
|
||||
PUBLIC BASE_FEATURE_ENABLED # пойдёт вглубь цепочки
|
||||
PRIVATE BASE_INTERNAL # остановится здесь
|
||||
)
|
||||
|
||||
add_library(lib_mid STATIC mid.c)
|
||||
target_link_libraries(lib_mid PUBLIC lib_base)
|
||||
# ^^^^^^
|
||||
# PUBLIC здесь означает: "lib_mid использует lib_base,
|
||||
# и мои потребители тоже должны о ней знать"
|
||||
|
||||
add_executable(app main.c)
|
||||
target_link_libraries(app PRIVATE lib_mid)
|
||||
```
|
||||
|
||||
**Что видит `app`:**
|
||||
|
||||
| Дефайн | Виден в app? | Причина |
|
||||
|-----------------------|:------------:|--------------------------------------------------|
|
||||
| `BASE_FEATURE_ENABLED`| ✅ | PUBLIC в lib_base → PUBLIC в lib_mid → app |
|
||||
| `BASE_INTERNAL` | ❌ | PRIVATE в lib_base, цепочка обрывается |
|
||||
|
||||
Если бы `lib_mid` слинковался с `lib_base` через `PRIVATE`:
|
||||
|
||||
```cmake
|
||||
target_link_libraries(lib_mid PRIVATE lib_base)
|
||||
# тогда app НЕ увидел бы BASE_FEATURE_ENABLED — цепочка оборвалась бы на lib_mid
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Применимость к командам
|
||||
|
||||
Ключевые слова работают одинаково во всех `target_*` командах:
|
||||
|
||||
| Команда | Типичное использование |
|
||||
|--------------------------------|----------------------------------------------------------|
|
||||
| `target_include_directories` | PRIVATE — внутренние папки; PUBLIC — публичные заголовки |
|
||||
| `target_compile_definitions` | PRIVATE — отладочные дефайны; PUBLIC — версии API |
|
||||
| `target_compile_options` | PRIVATE — флаги оптимизации; INTERFACE — флаги МК |
|
||||
| `target_link_libraries` | PRIVATE — внутр. зависимость; PUBLIC — транзитивная |
|
||||
| `target_link_options` | INTERFACE — скрипт линкера для всей цепочки |
|
||||
| `target_sources` | Всегда PRIVATE — исходники не передаются потребителям |
|
||||
|
||||
---
|
||||
|
||||
## Правило выбора
|
||||
|
||||
```bash
|
||||
Нужно ли это свойство самому таргету?
|
||||
│
|
||||
ДА ──→ Нужно ли оно потребителям?
|
||||
│ │
|
||||
│ ДА ──→ PUBLIC
|
||||
│ │
|
||||
│ НЕТ ──→ PRIVATE
|
||||
│
|
||||
НЕТ ──→ Нужно ли оно потребителям?
|
||||
│
|
||||
ДА ──→ INTERFACE
|
||||
│
|
||||
НЕТ ──→ (не добавляйте это свойство вообще)
|
||||
```
|
||||
|
||||
---
|
||||
433
docs/DEV_ARCH.md
433
docs/DEV_ARCH.md
|
|
@ -14,8 +14,8 @@
|
|||
форматирование, host-тесты, сборка HIL target-прошивок, подготовка HAB-образов.
|
||||
Управляется через VSCode tasks и модуль `just build::`.
|
||||
|
||||
**Хост** — всё что касается железа: прошивка платы через USB, HIL-тесты
|
||||
через pyOCD + pytest, отладка через GDB-сервер.
|
||||
**Хост** — всё что касается железа: прошивка платы через USB или SWD,
|
||||
HIL-тесты через pyOCD + pytest, GDB-сервер для отладки.
|
||||
Управляется через модуль `just host::`.
|
||||
|
||||
---
|
||||
|
|
@ -28,17 +28,16 @@
|
|||
├── Хост (Linux / macOS / Windows + Git Bash)
|
||||
│ ├── just ← запуск задач хостового уровня (just host::*)
|
||||
│ ├── docker ← управление devcontainer
|
||||
│ ├── uv + spsdk ← прошивка платы (flash_usb.py, sdphost, blhost)
|
||||
│ │ venv: tools/host/.venv-host (Linux/macOS)
|
||||
│ │ tools/host/.venv-host-win (Windows)
|
||||
│ ├── uv + pyocd ← HIL-тесты (tools/hil/.venv)
|
||||
│ │ + pyserial
|
||||
│ ├── uv + spsdk ← прошивка через USB ROM (flash_usb.py, sdphost, blhost)
|
||||
│ │ venv: tools/host/
|
||||
│ ├── uv + pyocd ← GDB-сервер отладки + HIL-тесты
|
||||
│ │ + pyserial venv: tools/hil/
|
||||
│ │ + pytest
|
||||
│ ├── JLinkGDBServer / probe-rs ← сервер отладки (USB → TCP :2331)
|
||||
│ │ + mpremote ← деплой агента на M5StampPLC
|
||||
│ └── VSCode ← IDE (Dev Containers extension)
|
||||
│
|
||||
├── Devcontainer (Docker)
|
||||
│ ├── ARM GCC 13.3 ← кросс-компилятор (firmware + HIL target-прошивки)
|
||||
│ ├── arm-none-eabi-gcc ← кросс-компилятор (firmware + HIL target-прошивки)
|
||||
│ ├── cmake + ninja ← система сборки
|
||||
│ ├── clang-17 ← компилятор для host-тестов
|
||||
│ ├── clangd-17 ← LSP (автодополнение, диагностика)
|
||||
|
|
@ -48,61 +47,75 @@
|
|||
│ ├── uv + spsdk ← сборка HAB-образов (только nxpimage)
|
||||
│ └── Unity + fff ← фреймворки host-тестов
|
||||
│
|
||||
└── Плата TFT (IMXRT1052)
|
||||
├── USB ──────────────────▶ хост (SDP-режим, прошивка)
|
||||
├── NXP MCU-Link (USB) ───▶ хост (CMSIS-DAP SWD + VCOM)
|
||||
│ ├── SWD ← pyOCD загружает HIL ELF в RAM
|
||||
│ └── VCOM ← pytest общается с прошивкой через UART
|
||||
├── SWD ──────────────────▶ JLink/probe-rs на хосте (отладка)
|
||||
└── CAN / UART / IO ──────▶ локальный стенд (будущие HIL-тесты)
|
||||
├── Плата TFT (MIMXRT1052)
|
||||
│ ├── USB ──────────────────────▶ хост (SDP-режим, прошивка через ROM)
|
||||
│ └── MCU-Link (USB) ───────────▶ хост (CMSIS-DAP)
|
||||
│ ├── SWD ← pyOCD: GDB-сервер отладки + прошивка Flash + загрузка HIL ELF в RAM
|
||||
│ └── VCOM ← pytest общается с HIL прошивкой через UART CLI
|
||||
│
|
||||
└── HIL стенд (M5Stack StamPLC)
|
||||
├── USB ──────────────────────▶ хост (M5 агент, JSON-lines CLI)
|
||||
├── RLY1 ─────────────────────▶ VIN таргета (управление питанием)
|
||||
├── RLY2 ─────────────────────▶ RS_RX таргета (BSP_OPTO_CH_RS)
|
||||
├── RLY3 ─────────────────────▶ EXT_IN1 таргета (BSP_OPTO_CH_IN1)
|
||||
└── RLY4 ─────────────────────▶ EXT_IN2 таргета (BSP_OPTO_CH_IN2)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Что устанавливается и где
|
||||
|
||||
| Инструмент | Хост | Devcontainer | Сервер |
|
||||
|------------|------|--------------|--------|
|
||||
| `just` | ✅ | ✅ Dockerfile | ✅ |
|
||||
| `docker` | ✅ | — | — |
|
||||
| `uv` | ✅ | ✅ Dockerfile | ✅ |
|
||||
| `spsdk` | ✅ uv sync | ✅ uv sync | ✅ uv sync |
|
||||
| `pyocd` + `pyserial` + `pytest` | ✅ uv sync | — | ✅ uv sync |
|
||||
| ARM GCC toolchain | — | ✅ | — |
|
||||
| `cmake` / `ninja` | — | ✅ | — |
|
||||
| `clang` / `clangd` | — | ✅ | — |
|
||||
| Unity / fff | — | ✅ | — |
|
||||
| JLink / probe-rs | ✅ | — | — |
|
||||
| Инструмент | Хост | Devcontainer |
|
||||
|------------|------|--------------|
|
||||
| `just` | ✅ | ✅ Dockerfile |
|
||||
| `docker` | ✅ | — |
|
||||
| `uv` | ✅ | ✅ Dockerfile |
|
||||
| `spsdk` (sdphost, blhost, nxpimage) | ✅ `tools/host/` | ✅ `tools/host/` |
|
||||
| `pyocd` + `pyserial` + `pytest` | ✅ `tools/hil/` | — |
|
||||
| `mpremote` | ✅ `tools/hil/` | — |
|
||||
| ARM GCC toolchain | — | ✅ |
|
||||
| `cmake` / `ninja` | — | ✅ |
|
||||
| `clang` / `clangd` / `clang-tidy` | — | ✅ |
|
||||
| Unity / fff | — | ✅ vendored |
|
||||
|
||||
`spsdk` и `pyocd` — разные venv с разными ролями:
|
||||
`spsdk` и `pyocd` — отдельные uv-проекты с разными ролями:
|
||||
|
||||
- `tools/host/` — `spsdk`: `nxpimage` + `sdphost` + `blhost` — прошивка через USB ROM
|
||||
- `tools/hil/` — `pyocd` + `pyserial` + `pytest` — HIL-тесты через SWD + VCOM
|
||||
- `tools/host/` — `spsdk`: `nxpimage` + `sdphost` + `blhost` — прошивка через USB ROM и HAB-образы
|
||||
- `tools/hil/` — `pyocd` + `pyserial` + `pytest` + `mpremote` — GDB-сервер, HIL-тесты, деплой M5 агента
|
||||
|
||||
---
|
||||
|
||||
## 3.1 Конфигурация проекта — `.env`
|
||||
|
||||
`.env` в корне репозитория — единый источник конфигурации для всего стека.
|
||||
`.env.example` — шаблон без значений, коммитится в репозиторий.
|
||||
|
||||
```ini
|
||||
# USB VID:PID (NXP)
|
||||
# Hardware
|
||||
BOARD=MIMXRT1052
|
||||
|
||||
# USB VID:PID (NXP ROM + Flashloader)
|
||||
BOOTROM_VID=1fc9
|
||||
BOOTROM_PID=0130
|
||||
FLASHLOADER_VID=15a2
|
||||
FLASHLOADER_PID=0073
|
||||
|
||||
# GDB / отладка
|
||||
# Debug / SWD
|
||||
GDB_PORT=3333
|
||||
GDB_EXECUTABLE=gdb-multiarch
|
||||
OPENOCD_INTERFACE=cmsis-dap.cfg
|
||||
TARGET_CFG=target/imxrt.cfg
|
||||
GDB_EXECUTABLE=arm-none-eabi-gdb
|
||||
PYOCD_TARGET=mimxrt1050_quadspi
|
||||
PYOCD_FREQUENCY=4000000
|
||||
FCB_PATH=tools/host/dcd/w25q128_fdcb.bin
|
||||
|
||||
# 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 МГц
|
||||
HIL_VCOM_PORT=/dev/cu.usbmodemXXXX # MCU-Link VCOM (macOS: cu.usbmodem*, Linux: ttyACM*)
|
||||
HIL_M5_PORT=/dev/cu.usbmodemYYYY # M5StampPLC USB CDC
|
||||
HIL_VCOM_BAUD=115200
|
||||
HIL_M5_BAUD=115200
|
||||
HIL_READY_TIMEOUT=5.0
|
||||
HIL_M5_TIMEOUT=3.0
|
||||
HIL_PYOCD_FREQUENCY=1000000
|
||||
HIL_BUILD_DIR=build/target-debug
|
||||
```
|
||||
|
||||
**Как значения попадают в инструменты:**
|
||||
|
|
@ -111,16 +124,15 @@ HIL_VCOM_PORT=/dev/tty.usbmodemXXXX # VCOM-порт MCU-Link (macOS/Linux)
|
|||
.env
|
||||
│
|
||||
├─▶ just (set dotenv-load + set export)
|
||||
│ ├─▶ just-рецепты: {{BOOTROM_VID}}, {{HIL_VCOM_PORT}}
|
||||
│ ├─▶ just-рецепты: {{BOOTROM_VID}}, {{HIL_VCOM_PORT}}, {{GDB_PORT}}
|
||||
│ └─▶ uv run python ← наследует os.environ автоматически
|
||||
│ ├─▶ flash_usb.py: os.environ.get("BOOTROM_VID")
|
||||
│ └─▶ env_config.py: os.environ.get("HIL_VCOM_PORT")
|
||||
│ ├─▶ flash_usb.py: os.environ["BOOTROM_VID"]
|
||||
│ ├─▶ flash_swd.py: os.environ["PYOCD_TARGET"]
|
||||
│ └─▶ env_config.py: os.environ["HIL_VCOM_PORT"]
|
||||
│
|
||||
└─▶ .vscode/launch.json ← через ${env:GDB_PORT}
|
||||
```
|
||||
|
||||
`.env.example` — шаблон без значений, коммитится в репозиторий.
|
||||
|
||||
---
|
||||
|
||||
## 4. Структура automation-части репозитория
|
||||
|
|
@ -129,36 +141,73 @@ HIL_VCOM_PORT=/dev/tty.usbmodemXXXX # VCOM-порт MCU-Link (macOS/Linux)
|
|||
/
|
||||
├── justfile ← корневой оркестратор; модули: build, host, ci
|
||||
├── bootstrap.sh ← уровень 0: устанавливает just + uv → just host::bootstrap
|
||||
├── pyocd.yaml ← конфигурация pyOCD (target: cortex_m, RAM-режим)
|
||||
├── pyocd.yaml ← конфигурация pyOCD (HIL RAM-режим)
|
||||
├── pyocd_debug.yaml ← конфигурация pyOCD (GDB-сервер отладки)
|
||||
├── .env / .env.example
|
||||
│
|
||||
├── just/
|
||||
│ ├── build.just ← devcontainer: сборка firmware, host-тесты,
|
||||
│ │ HAB-образы, HIL target-прошивки
|
||||
│ ├── host.just ← хост: прошивка, bootstrap, HIL-тесты
|
||||
│ ├── host.just ← хост: прошивка (USB + SWD), HIL-тесты,
|
||||
│ │ GDB-сервер, M5StampPLC, bootstrap
|
||||
│ └── ci.just
|
||||
│
|
||||
├── .devcontainer/
|
||||
│ ├── Dockerfile
|
||||
│ └── devcontainer.json
|
||||
│
|
||||
├── .vscode/
|
||||
│ ├── launch.json ← cortex-debug конфигурации (firmware_test, bootloader, tft_app)
|
||||
│ └── tasks.json ← UI для just build::* (внутри devcontainer)
|
||||
│
|
||||
├── tools/
|
||||
│ ├── host/ ← spsdk-окружение (прошивка через USB ROM)
|
||||
│ │ ├── flash_usb.py
|
||||
│ │ ├── hab/ ← HAB yaml-конфиги
|
||||
│ │ ├── dcd/ ← ivt_flashloader.bin, dcd.bin
|
||||
│ │ ├── flash_usb.py ← USB SDP: sdphost + blhost
|
||||
│ │ ├── flash_swd.py ← SWD: FCB + HAB → pyOCD Flash
|
||||
│ │ ├── hab/ ← HAB yaml-конфиги (nxpimage)
|
||||
│ │ ├── dcd/ ← w25q128_fdcb.bin, ivt_flashloader.bin
|
||||
│ │ └── uv.lock
|
||||
│ │
|
||||
│ └── hil/ ← HIL pytest-окружение
|
||||
│ ├── conftest.py ← фикстуры: loaded_<n>, uart
|
||||
│ ├── pyocd_utils.py ← FLEXRAM, ELF loader, run_from_vectors
|
||||
│ ├── env_config.py ← конфигурация из os.environ
|
||||
│ ├── load_and_run.py ← CLI-утилита загрузки ELF
|
||||
│ ├── test_uart.py ← HIL тесты bsp_uart_host
|
||||
│ ├── conftest.py ← фикстуры: m5, loaded_<n>, uart_<n>
|
||||
│ ├── 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 ← HIL тест bsp_uart_host (без M5)
|
||||
│ ├── test_opto.py ← HIL тест bsp_opto (через M5StampPLC)
|
||||
│ ├── m5/
|
||||
│ │ ├── agent.py ← MicroPython агент на M5StampPLC
|
||||
│ │ ├── cli.py ← интерактивный CLI для ручного тестирования стенда
|
||||
│ │ └── power.py ← управление питанием таргета из командной строки
|
||||
│ └── uv.lock
|
||||
│
|
||||
├── CMakePresets.json ← Debug · Release · host-debug · target-debug
|
||||
└── tests/
|
||||
├── host/ ← host unit-тесты (Unity + fff)
|
||||
└── target/ ← HIL target-прошивки (RAM, pyOCD)
|
||||
└── host_uart/ ← CLI для тестирования UART
|
||||
│
|
||||
├── tests/
|
||||
│ ├── host/ ← host unit-тесты (Unity + fff)
|
||||
│ │ ├── mocks/ ← stub-хедеры NXP SDK для компиляции на хосте
|
||||
│ │ ├── led/
|
||||
│ │ ├── opto/
|
||||
│ │ ├── ring_buffer/
|
||||
│ │ ├── timeout/
|
||||
│ │ └── uart_host/
|
||||
│ └── target/ ← HIL target-прошивки (RAM, pyOCD)
|
||||
│ ├── host_uart/ ← CLI для тестирования bsp_uart_host
|
||||
│ └── hil_opto/ ← CLI для тестирования bsp_opto
|
||||
│
|
||||
└── docs/
|
||||
├── DEV_ARCH.md ← этот документ
|
||||
├── HOW_TO_FLASH.md ← прошивка (USB SDP + SWD)
|
||||
├── HOW_TO_DEBUG.md ← отладка (GDB + RTT + FreeRTOS)
|
||||
├── hardware/ ← схемы, datasheet платы
|
||||
├── mimxrt1052/ ← MCU: BOOT_FLAGS, reference manual
|
||||
└── testing/
|
||||
├── hil/
|
||||
│ ├── HIL_HOWTO.md ← как проводить HIL-тесты
|
||||
│ ├── HIL_BENCH.md ← стенд: оборудование, подключение
|
||||
│ └── HIL_CREATE_TEST.md ← как добавить новый HIL-тест
|
||||
└── host/
|
||||
└── HOST_CREATE_TEST.md ← как добавить host unit-тест
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -186,36 +235,32 @@ git clone <repo-url> && cd <repo>
|
|||
bootstrap.sh (уровень 0)
|
||||
│
|
||||
├── определить платформу (Linux / macOS / Windows Git Bash)
|
||||
├── проверить/установить just >= 1.36.0
|
||||
├── проверить/установить uv >= 0.4.0
|
||||
├── проверить/установить just >= 1.36.0 (через uv tool)
|
||||
│
|
||||
└── exec just host::bootstrap
|
||||
├── [1/3] check-deps — just · uv · docker
|
||||
├── [2/3] setup-udev — udev-правила NXP USB (только Linux)
|
||||
│ 1FC9:0130 ← BootROM SDP
|
||||
│ 15A2:0073 ← Flashloader
|
||||
│ dialout ← группа для /dev/ttyACM* (M5StampPLC)
|
||||
└── [3/3] setup-tools — uv sync в tools/host/
|
||||
SHA-256 uv.lock кешируется → повторный вызов мгновенный
|
||||
```
|
||||
|
||||
### 5.4 Настройка HIL-окружения (один раз, на хосте)
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
### 5.5 После bootstrap
|
||||
### 5.4 После bootstrap
|
||||
|
||||
```bash
|
||||
# Открыть VSCode → "Reopen in Container"
|
||||
# postCreateCommand выполняется автоматически:
|
||||
# uv sync (tools/host) + cmake --preset host-debug + cmake --preset Debug
|
||||
|
||||
# Настроить .env (один раз):
|
||||
cp .env.example .env
|
||||
# Заполнить HIL_VCOM_PORT и HIL_M5_PORT под свои порты
|
||||
|
||||
# Задеплоить агент на M5StampPLC (один раз, и каждый раз после изменений в agent.py):
|
||||
just host::m5-deploy
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -227,7 +272,7 @@ uv sync # установить pyocd, pyserial, pytest
|
|||
| Пресет | Toolchain | Назначение | Линкер-скрипт |
|
||||
|--------|-----------|------------|---------------|
|
||||
| `Debug` / `Release` | ARM GCC | firmware_test, bootloader, tft_app | `flexspi_nor.ld` |
|
||||
| `host-debug` / `host-release` | clang (host) | Unity + fff тесты | — |
|
||||
| `host-debug` / `host-release` | clang (хост) | Unity + fff тесты | — |
|
||||
| `target-debug` | ARM GCC | HIL target-прошивки | `ram.ld` |
|
||||
|
||||
### 6.2 CMake пресеты
|
||||
|
|
@ -245,22 +290,24 @@ buildPresets (ARM firmware):
|
|||
all-debug / all-release
|
||||
|
||||
buildPresets (host-тесты):
|
||||
host-debug-build / host-release-build
|
||||
host-debug-build: test_bsp_led, test_log, test_bsp_opto,
|
||||
test_ring_buffer, test_timeout_pattern, uart_host_mock_example
|
||||
host-release-build: то же
|
||||
|
||||
buildPresets (HIL):
|
||||
target-debug-build ← test_host_uart (и будущие HIL-прошивки)
|
||||
target-debug-build: test_host_uart, test_hil_opto
|
||||
```
|
||||
|
||||
### 6.3 Boot-стратегии
|
||||
|
||||
| Прошивка | Стратегия | DCD | Инструмент загрузки |
|
||||
|----------|-----------|-----|---------------------|
|
||||
| `firmware_test` | XIP из Flash | ✅ | SPSDK → Flash |
|
||||
| `bootloader` | Копирование в ITCM | ❌ | SPSDK → Flash |
|
||||
| `tft_app` | XIP + буферы в SDRAM | ✅ | SPSDK → Flash |
|
||||
| HIL target (`tests/target/`) | Исполнение из ITCM/DTCM | ❌ | pyOCD → RAM |
|
||||
| Прошивка | Стратегия | Инструмент загрузки |
|
||||
|----------|-----------|---------------------|
|
||||
| `firmware_test` | XIP из Flash (`flexspi_nor.ld`) | SPSDK → Flash |
|
||||
| `bootloader` | Копирование в ITCM | SPSDK → Flash |
|
||||
| `tft_app` | XIP + буферы в SDRAM | SPSDK → Flash |
|
||||
| HIL target (`tests/target/`) | Исполнение из ITCM/DTCM (`ram.ld`) | pyOCD → RAM |
|
||||
|
||||
**HIL boot-стратегия (`bsp_boot_ram`):** pyOCD настраивает FLEXRAM (128KB ITCM + 128KB DTCM), записывает сегменты ELF по физическим адресам, устанавливает SP/PC из таблицы векторов и запускает выполнение. Flash не используется.
|
||||
**HIL boot-стратегия:** pyOCD настраивает FLEXRAM (128 KB ITCM + 128 KB DTCM + 256 KB OCRAM), записывает PT_LOAD сегменты ELF по физическим адресам, устанавливает SP/PC из таблицы векторов и запускает выполнение. Flash не используется — прошивка исчезает при отключении питания.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -276,29 +323,38 @@ buildPresets (HIL):
|
|||
| Сборка ARM firmware (ELF) | devcontainer |
|
||||
| Сборка HIL target-прошивок | devcontainer |
|
||||
| Подготовка HAB-образов (nxpimage) | devcontainer |
|
||||
| Прошивка платы через USB ROM | **хост** |
|
||||
| HIL-тесты (pyOCD + pytest) | **хост** |
|
||||
| Отладка — GDB-сервер (JLink) | **хост** |
|
||||
| Отладка — GDB-клиент | devcontainer → хост по TCP |
|
||||
| Прошивка платы через USB ROM | хост |
|
||||
| Прошивка платы через SWD | хост |
|
||||
| HIL-тесты (pyOCD + pytest + M5) | хост |
|
||||
| Отладка — GDB-сервер (pyOCD) | хост |
|
||||
| Отладка — GDB-клиент (cortex-debug) | devcontainer → хост по TCP |
|
||||
|
||||
### 7.2 Типичная сессия разработки
|
||||
|
||||
```bash
|
||||
# Devcontainer (терминал VSCode)
|
||||
# ── Devcontainer (терминал VSCode) ──────────────────────────────────────────
|
||||
|
||||
just build::test-host # host unit-тесты — зелёные?
|
||||
just build::build-firmware-test-debug # ELF собирается?
|
||||
just build::hab-firmware-test-debug # HAB-образ готов
|
||||
just build::hab-firmware-test-debug # HAB-образ для прошивки
|
||||
|
||||
# Хостовый терминал — прошивка
|
||||
just flash # прошить firmware_test debug во Flash
|
||||
# ── Хостовый терминал — прошивка ─────────────────────────────────────────────
|
||||
|
||||
# Хостовый терминал — HIL
|
||||
just build::build-hil # собрать HIL target-прошивку
|
||||
# (можно делать в devcontainer)
|
||||
just host::hil-run # загрузить ELF → запустить pytest
|
||||
just host::flash-test-debug # прошить через USB SDP
|
||||
# или
|
||||
just host::flash-swd-test-debug # прошить через SWD (power cycle после)
|
||||
|
||||
# Отладка
|
||||
# F5 в VSCode → запустить JLinkGDBServer на хосте → attach через cortex-debug
|
||||
# ── Хостовый терминал — HIL ──────────────────────────────────────────────────
|
||||
|
||||
just build::build-hil # (devcontainer) собрать HIL ELF
|
||||
just host::hil-run # загрузить ELF → запустить все тесты
|
||||
just host::hil-uart # только UART тесты
|
||||
just host::hil-opto # только opto тесты (нужен стенд M5)
|
||||
|
||||
# ── Отладка ───────────────────────────────────────────────────────────────────
|
||||
|
||||
just host::debug-server # запустить pyOCD GDB-сервер на хосте
|
||||
# F5 в VSCode → 🐛 Debug: firmware_test
|
||||
```
|
||||
|
||||
### 7.3 VSCode Tasks (внутри devcontainer)
|
||||
|
|
@ -317,68 +373,68 @@ just host::hil-run # загрузить ELF → запус
|
|||
|
||||
## 8. Прошивка платы (хост)
|
||||
|
||||
### Перевод в SDP-режим
|
||||
Подробно — [docs/HOW_TO_FLASH.md](HOW_TO_FLASH.md). Краткая сводка:
|
||||
|
||||
### USB SDP (Serial Download Protocol)
|
||||
|
||||
Требует перевода платы в SDP-режим (`BOOT_MOD_1 → 3V3 → Reset`).
|
||||
|
||||
```bash
|
||||
1. BOOT_MOD_1 → 3V3
|
||||
2. Reset
|
||||
3. Подключить USB → плата как VID:PID 1FC9:0130
|
||||
4. just host::flash <project> <type>
|
||||
5. После прошивки: BOOT_MOD_1 → GND → Reset
|
||||
just host::flash-test-debug # firmware_test Debug → Flash
|
||||
just host::flash-test-release # firmware_test Release → Flash
|
||||
just host::flash-production # bootloader + tft_app Release (с подтверждением)
|
||||
```
|
||||
|
||||
### Команды прошивки
|
||||
### SWD (через MCU-Link, без смены режима загрузки)
|
||||
|
||||
```bash
|
||||
# project × type
|
||||
just host::flash firmware_test debug
|
||||
just host::flash firmware_test release
|
||||
just host::flash bootloader release
|
||||
just host::flash tft_app release
|
||||
|
||||
# В RAM — быстро, без износа Flash
|
||||
just host::flash-ram firmware_test
|
||||
|
||||
# Псевдонимы
|
||||
just flash # = firmware_test debug → Flash
|
||||
just host::flash-production # bootloader + tft_app release
|
||||
just host::flash-swd-test-debug
|
||||
just host::flash-swd-test-release
|
||||
# После любого flash-swd — обязательный power cycle платы
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. HIL-тесты (хост)
|
||||
|
||||
HIL-тесты проверяют периферию на реальном железе. MCU-Link обеспечивает два канала по одному USB: SWD (прошивка через pyOCD) и VCOM (UART CLI).
|
||||
HIL-тесты проверяют периферию на реальном железе. Два типа:
|
||||
|
||||
### Базовые (без стенда) — `test_uart.py`
|
||||
|
||||
Только MCU-Link: SWD загружает ELF в RAM, VCOM обеспечивает UART CLI.
|
||||
|
||||
```bash
|
||||
pytest → uart_cmd("PING\r\n")
|
||||
pytest → uart_cmd("PING")
|
||||
↓ pyserial / VCOM
|
||||
MCU-Link
|
||||
↓ LPUART1
|
||||
RT1052 (HIL прошивка)
|
||||
→ "PONG\r\n"
|
||||
RT1052 → "PONG"
|
||||
```
|
||||
|
||||
### С M5StampPLC — `test_opto.py` и другие
|
||||
|
||||
`M5StampPLC` управляет входными сигналами таргета через реле. pytest оркестрирует оба канала одновременно.
|
||||
|
||||
```bash
|
||||
pytest
|
||||
├─▶ m5.opto_set(1, True) → M5 (JSON) → RLY3 → EXT_IN1 таргета
|
||||
└─▶ uart_cmd("OPTO_READ 1") → MCU-Link VCOM → RT1052 → "ACTIVE"
|
||||
```
|
||||
|
||||
Перед каждой тест-сессией фикстура `m5` автоматически включает питание таргета (RLY1), ждёт стабилизации, затем `loaded_<n>` загружает ELF через pyOCD.
|
||||
|
||||
### Команды
|
||||
|
||||
```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)
|
||||
just build::build-hil # (devcontainer) собрать HIL ELF
|
||||
just host::hil-run # прогнать все HIL тесты
|
||||
just host::hil-uart # только test_uart.py
|
||||
just host::hil-opto # только test_opto.py
|
||||
```
|
||||
|
||||
### Архитектура 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).
|
||||
Подробно — [docs/testing/hil/HIL_HOWTO.md](testing/hil/HIL_HOWTO.md).
|
||||
Добавление нового теста — [docs/testing/hil/HIL_CREATE_TEST.md](testing/hil/HIL_CREATE_TEST.md).
|
||||
Стенд и подключение — [docs/testing/hil/HIL_BENCH.md](testing/hil/HIL_BENCH.md).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -390,58 +446,51 @@ just host::hil-load # только загрузить ELF (без pytest
|
|||
Фреймворк: Unity + fff
|
||||
Пресеты: host-debug / host-release
|
||||
Компилятор: clang-17 (не ARM GCC)
|
||||
Запуск: just build::test-host
|
||||
Запуск: just build::test-host (внутри devcontainer)
|
||||
```
|
||||
|
||||
BSP-модули тестируются через fff-фейки и stub-хедеры в `tests/host/mocks/`. `BUILD_TESTS_HOST=ON` отключает ARM-специфику.
|
||||
BSP-модули тестируются через fff-фейки и stub-хедеры в `tests/host/mocks/`.
|
||||
`BUILD_TESTS_HOST=ON` отключает ARM-специфику и SDK-заголовки.
|
||||
|
||||
Покрытие: `bsp_led`, `bsp_opto`, `bsp_uart_host`, `ring_buffer`, timeout-паттерн.
|
||||
Гайд — [tests/HOST_CREATE_TEST.md](../tests/HOST_CREATE_TEST.md).
|
||||
|
||||
### 10.2 HIL target-тесты
|
||||
|
||||
```bash
|
||||
Инструменты: pyOCD (SWD) + pyserial (UART) + pytest
|
||||
Инструменты: pyOCD (SWD) + pyserial (UART) + pytest + M5StampPLC (реле)
|
||||
Пресет: target-debug → ram.ld → ITCM/DTCM
|
||||
Запуск: just host::hil-run
|
||||
Запуск: 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 работает одинаково с обоими каналами.
|
||||
Текущие тесты: `test_uart.py` (PING/ECHO/BUF_SIZE), `test_opto.py` (оптовходы IN1/IN2/RS через M5).
|
||||
|
||||
---
|
||||
|
||||
## 11. Отладка
|
||||
|
||||
Подробно — [docs/HOW_TO_DEBUG.md](HOW_TO_DEBUG.md). Краткая схема:
|
||||
|
||||
```bash
|
||||
Хост
|
||||
├── JLinkGDBServer -device MIMXRT1052 -if SWD -port 2331
|
||||
│ USB/SWD → плата
|
||||
└── host.docker.internal:2331 ← доступен из devcontainer
|
||||
├── just host::debug-server
|
||||
│ └── pyocd gdbserver :3333
|
||||
│ USB/SWD → MCU-Link → плата
|
||||
└── host.docker.internal:3333 ← доступен из devcontainer
|
||||
|
||||
Devcontainer
|
||||
└── arm-none-eabi-gdb
|
||||
target remote host.docker.internal:2331
|
||||
└── cortex-debug (VSCode)
|
||||
↔ arm-none-eabi-gdb
|
||||
target remote host.docker.internal:3333
|
||||
```
|
||||
|
||||
`.vscode/launch.json` (cortex-debug, тип `external`):
|
||||
Три конфигурации в `.vscode/launch.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "cortex-debug",
|
||||
"servertype": "external",
|
||||
"gdbTarget": "host.docker.internal:2331",
|
||||
"executable": "${workspaceFolder}/build/Debug/firmware/test/firmware_test.elf"
|
||||
}
|
||||
```
|
||||
- `🐛 Debug: firmware_test` — bare-metal, входной контроль
|
||||
- `🐛 Debug: bootloader` — bare-metal, A/B обновление
|
||||
- `🐛 Debug: tft_app (FreeRTOS)` — FreeRTOS task view
|
||||
|
||||
RTT-логи (`SEGGER_RTT_ENABLED=ON`) пока что не используются вместо этого можно использовать `port/log`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -456,14 +505,14 @@ feature-ветка
|
|||
│ just build::build-hil ← HIL-прошивки собираются?
|
||||
│
|
||||
├── хост
|
||||
│ just flash ← прошить, проверить на железе
|
||||
│ just host::flash-test-debug ← прошить, проверить на железе
|
||||
│ just host::hil-run ← HIL зелёные?
|
||||
│
|
||||
├── подготовка к MR
|
||||
│ just build::hab-all-release
|
||||
│ just host::flash firmware_test release
|
||||
│
|
||||
└── Merge Request → GitLab CI
|
||||
└── Merge Request → CI
|
||||
host-тесты · сборка · HIL · публикация артефактов
|
||||
↓
|
||||
Производственный сервер
|
||||
|
|
@ -490,55 +539,3 @@ feature-ветка
|
|||
Установлено: just · uv + spsdk · uv + pyocd/pytest · git
|
||||
НЕ установлено: docker · cmake · компилятор · ARM toolchain
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Приложение А: минимальные версии
|
||||
|
||||
| Инструмент | Версия | Причина |
|
||||
|------------|--------|---------|
|
||||
| `just` | 1.36.0 | поддержка `mod` с кастомным путём |
|
||||
| `uv` | 0.4.0 | стабильный lockfile формат |
|
||||
| `docker` | 24.0.0 | Compose v2 |
|
||||
| `spsdk` | 3.7.x | совместимость с HAB yaml-форматом |
|
||||
| `pyocd` | 0.36+ | cortex_m target, write_core_register API |
|
||||
| ARM GCC | 13.3.rel1 | C11, текущий SDK |
|
||||
| clang/clangd | 17 | поддержка `If:` в `.clangd` |
|
||||
|
||||
---
|
||||
|
||||
## Приложение Б: шпаргалка
|
||||
|
||||
```bash
|
||||
# ── Первый запуск ───────────────────────────────────────────
|
||||
./bootstrap.sh
|
||||
cd tools/hil && uv sync
|
||||
# VSCode → Reopen in Container
|
||||
|
||||
# ── devcontainer ────────────────────────────────────────────
|
||||
just build::test-host # host unit-тесты
|
||||
just build::build-firmware-test-debug # сборка firmware
|
||||
just build::build-hil # сборка HIL target-прошивок
|
||||
just build::hab-firmware-test-debug # HAB-образ Debug
|
||||
just build::hab-all-release # все HAB Release
|
||||
just build::clean # очистить build/
|
||||
|
||||
# ── хост — прошивка ─────────────────────────────────────────
|
||||
just flash # firmware_test debug → Flash
|
||||
just host::flash firmware_test release
|
||||
just host::flash bootloader release
|
||||
just host::flash-production # bootloader + tft_app release
|
||||
just host::flash-ram firmware_test # в RAM (без износа Flash)
|
||||
|
||||
# ── хост — HIL-тесты ────────────────────────────────────────
|
||||
just host::hil-run # загрузить ELF + все тесты
|
||||
just host::hil-smoke # только smoke
|
||||
just host::hil-run-fast # без перезагрузки ELF
|
||||
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
|
||||
```
|
||||
|
|
|
|||
188
docs/testing/hil/HIL_BENCH.md
Normal file
188
docs/testing/hil/HIL_BENCH.md
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
# HIL Стенд
|
||||
|
||||
Описание аппаратного стенда, подключений, инструментов и их роли в HIL-тестировании.
|
||||
|
||||
---
|
||||
|
||||
## Оборудование
|
||||
|
||||
| Устройство | Роль |
|
||||
|-------------------|-------------------------------------------------------------|
|
||||
| MIMXRT1052CVJ5B | Таргет — плата под тестом |
|
||||
| M5Stack StamPLC | Промежуточная платформа стенда: питание таргета + сигналы |
|
||||
| MCU-Link (CMSIS-DAP) | SWD-probe + VCOM (pyOCD загружает ELF, pytest читает UART) |
|
||||
|
||||
---
|
||||
|
||||
## Подключение
|
||||
|
||||
### Питание таргета
|
||||
|
||||
| M5 реле | Куда | Назначение |
|
||||
|---------|------------------|---------------------|
|
||||
| RLY1 | VIN таргета | Управление питанием |
|
||||
|
||||
Питание включается и выключается автоматически фикстурой `m5` в `conftest.py`:
|
||||
|
||||
- `agent.power(True)` в setup — до загрузки ELF через pyOCD
|
||||
- `agent.power(False)` в teardown — после завершения всех тестов модуля
|
||||
|
||||
### Оптоизолированные входы
|
||||
|
||||
Таргет: оптопары PS2801-4, **неинвертирующие** (active-HIGH).
|
||||
M5: реле AW9523B через ULN2003A, нормально разомкнутые (NO).
|
||||
|
||||
| M5 реле | Сигнал таргета | BSP канал | MCU пин | GPIO |
|
||||
|---------|----------------|--------------------|---------------|-----------|
|
||||
| RLY2 | RS_RX | `BSP_OPTO_CH_RS` | GPIO_AD_B1_07 | GPIO1[23] |
|
||||
| RLY3 | EXT_IN1 | `BSP_OPTO_CH_IN1` | GPIO_AD_B1_06 | GPIO1[22] |
|
||||
| RLY4 | EXT_IN2 | `BSP_OPTO_CH_IN2` | GPIO_AD_B1_05 | GPIO1[21] |
|
||||
|
||||
**Логика сигнала:**
|
||||
- Реле разомкнуто → нет тока через оптопару → пин LOW → `BSP_OPTO_STATE_INACTIVE`
|
||||
- Реле замкнуто → ток через оптопару → пин HIGH → `BSP_OPTO_STATE_ACTIVE`
|
||||
|
||||
Маппинг зафиксирован в `tools/hil/m5/agent.py`:
|
||||
|
||||
```python
|
||||
_OPTO_TO_RELAY = {1: 3, 2: 4, 3: 2}
|
||||
# ch1 (EXT_IN1 / BSP_OPTO_CH_IN1) → RLY3
|
||||
# ch2 (EXT_IN2 / BSP_OPTO_CH_IN2) → RLY4
|
||||
# ch3 (RS_RX / BSP_OPTO_CH_RS ) → RLY2
|
||||
```
|
||||
|
||||
> **При изменении физической разводки** — обновить `_OPTO_TO_RELAY` в `agent.py`
|
||||
> и задеплоить: `just host::m5-deploy`.
|
||||
|
||||
### Порты и переменные окружения
|
||||
|
||||
| Переменная | Значение по умолчанию | Назначение |
|
||||
|----------------------|-----------------------|---------------------------|
|
||||
| `HIL_VCOM_PORT` | `/dev/ttyACM0` | MCU-Link VCOM (UART CLI) |
|
||||
| `HIL_VCOM_BAUD` | `115200` | Скорость UART CLI |
|
||||
| `HIL_M5_PORT` | `/dev/ttyACM1` | M5StampPLC USB CDC |
|
||||
| `HIL_M5_BAUD` | `115200` | Скорость M5 агента |
|
||||
| `HIL_BUILD_DIR` | `build/target-debug` | Путь к собранным ELF |
|
||||
|
||||
На macOS порты выглядят как `/dev/cu.usbmodem*`. Задаются в `.env` в корне репозитория.
|
||||
|
||||
---
|
||||
|
||||
## Инструменты стенда
|
||||
|
||||
### M5Stack StamPLC — `tools/hil/m5/`
|
||||
|
||||
| Файл | Назначение |
|
||||
|--------------|----------------------------------------------------------------|
|
||||
| `agent.py` | MicroPython агент на M5. Принимает JSON-команды через USB CDC, управляет реле через I2C экспандер AW9523B |
|
||||
| `cli.py` | Интерактивный CLI для ручного тестирования агента |
|
||||
| `power.py` | Скрипт управления питанием таргета (RLY1) из командной строки |
|
||||
|
||||
**Протокол агента:** JSON-lines через USB CDC (115200 бод).
|
||||
|
||||
```bash
|
||||
хост → M5: {"cmd": "opto_set", "ch": 1, "state": true}\r\n
|
||||
M5 → хост: {"ok": true, "opto_ch": 1, "relay": 3, "state": true}\r\n
|
||||
```
|
||||
|
||||
**Доступные команды агента:**
|
||||
|
||||
| Команда | Параметры | Действие |
|
||||
|----------------|------------------------|---------------------------------------|
|
||||
| `ping` | — | Проверка связи |
|
||||
| `power` | `state: bool` | RLY1 — питание таргета |
|
||||
| `relay_set` | `ch: 1-4, state: bool` | Прямое управление реле |
|
||||
| `relay_all_off`| — | Выключить все реле |
|
||||
| `opto_set` | `ch: 1-3, state: bool` | Управление оптоканалом таргета (через маппинг) |
|
||||
| `opto_all_off` | — | Выключить все оптоканалы |
|
||||
|
||||
**Деплой агента на M5:**
|
||||
|
||||
```bash
|
||||
just host::m5-deploy
|
||||
```
|
||||
|
||||
> ⚠️ После изменения `agent.py` обязательно задеплоить перед запуском тестов.
|
||||
> Иначе на M5 работает старая версия.
|
||||
|
||||
### MCU-Link — загрузка ELF и UART CLI
|
||||
|
||||
MCU-Link выполняет две роли одновременно:
|
||||
|
||||
**SWD (pyOCD)** — загружает `.elf` в RAM таргета перед каждой тест-сессией:
|
||||
|
||||
```bash
|
||||
pyOCD → MCU-Link SWD → MIMXRT1052
|
||||
halt → FLEXRAM init → load ELF → run_from_vectors
|
||||
```
|
||||
|
||||
**VCOM (pyserial)** — текстовый CLI для общения с прошивкой во время тестов:
|
||||
|
||||
```bash
|
||||
pytest → pyserial → MCU-Link VCOM → LPUART1 → прошивка таргета
|
||||
uart_cmd("PING") → "PONG"
|
||||
uart_cmd("OPTO_READ 1") → "ACTIVE"
|
||||
```
|
||||
|
||||
> ⚠️ MCU-Link занимает SWD монопольно. GDB-сервер (`just host::debug-server`)
|
||||
> и загрузка ELF через pyOCD не могут работать одновременно.
|
||||
|
||||
### pytest + conftest.py — оркестрация
|
||||
|
||||
`tools/hil/conftest.py` содержит всю логику подготовки стенда:
|
||||
|
||||
```
|
||||
фикстура m5 (scope=module)
|
||||
├── подключиться к M5 агенту
|
||||
├── agent.power(True) ← RLY1: питание таргета ON
|
||||
├── sleep(1.0 с) ← ждём POR + стабилизацию
|
||||
└── agent.opto_all_off() ← все сигнальные реле выключены
|
||||
|
||||
фикстура loaded_<n> (scope=module, зависит от m5)
|
||||
└── pyOCD: FLEXRAM → load ELF → run_from_vectors
|
||||
|
||||
фикстура uart_<n> (scope=module, зависит от loaded_<n>)
|
||||
└── открыть VCOM, ждать "READY\r\n" от прошивки
|
||||
|
||||
тесты (scope=function)
|
||||
└── uart_cmd() / m5.opto_set() / assert
|
||||
|
||||
teardown
|
||||
└── agent.opto_all_off() → agent.power(False) → ser.close()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Just-рецепты стенда
|
||||
|
||||
```bash
|
||||
# Деплой агента на M5 (после изменений agent.py)
|
||||
just host::m5-deploy
|
||||
|
||||
# Проверить что M5 видна в системе
|
||||
just host::m5-scan
|
||||
|
||||
# Открыть REPL на M5 для ручной отладки
|
||||
just host::m5-repl
|
||||
|
||||
# Интерактивный CLI для ручного тестирования агента
|
||||
just host::m5-cli
|
||||
|
||||
# Включить/выключить питание таргета вручную
|
||||
just host::m5-power on
|
||||
just host::m5-power off
|
||||
|
||||
# UART монитор — смотреть что шлёт прошивка
|
||||
just host::uart-monitor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Добавление нового сигнала стенда
|
||||
|
||||
1. Физически подключить сигнал к свободному реле M5StampPLC.
|
||||
2. Добавить маппинг в `agent.py` (по аналогии с `_OPTO_TO_RELAY`).
|
||||
3. Добавить команду в `_dispatch()` в `agent.py`.
|
||||
4. Добавить метод в класс `M5Agent` в `conftest.py`.
|
||||
5. Задеплоить: `just host::m5-deploy`.
|
||||
6. Обновить таблицу подключений выше.
|
||||
374
docs/testing/hil/HIL_CREATE_TEST.md
Normal file
374
docs/testing/hil/HIL_CREATE_TEST.md
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
# Добавление нового HIL-теста
|
||||
|
||||
## Обзор стека
|
||||
|
||||
```bash
|
||||
devcontainer хост
|
||||
───────────────────────────────── ────────────────────────────────────
|
||||
tests/target/<name>/ tools/hil/
|
||||
main.c ← C-прошивка с CLI test_<name>.py ← pytest-тесты
|
||||
CMakeLists.txt conftest.py ← фикстуры (общие)
|
||||
m5/agent.py ← агент M5 (если нужен)
|
||||
CMakePresets.json
|
||||
target-debug-build just/host.just
|
||||
└── targets: [test_<name>] hil-run, hil-<name>
|
||||
|
||||
just/build.just
|
||||
build-hil
|
||||
```
|
||||
|
||||
Два типа тестов:
|
||||
|
||||
| Тип | Использует M5 | Когда применять |
|
||||
|-----|--------------|-----------------|
|
||||
| **Базовый** | Нет | Тестирование UART CLI, алгоритмов, таймингов |
|
||||
| **С M5** | Да | Тестирование GPIO, оптовходов, реле, питания |
|
||||
|
||||
---
|
||||
|
||||
## Шаг 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 /* мс — увеличить если нужен частый process() */
|
||||
|
||||
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 (;;) {
|
||||
/* Если тест использует прерывания/process() — вызывать здесь */
|
||||
size_t len = cli_read_line(s_line_buf, sizeof(s_line_buf));
|
||||
if (len > 0U) cli_process_line((const char *)s_line_buf);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `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_opto / bsp_can / ... если нужно
|
||||
)
|
||||
|
||||
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(hil_opto)
|
||||
add_subdirectory(<name>) # ← добавить строку
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 3 — `CMakePresets.json`: добавить таргет
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "target-debug-build",
|
||||
"configurePreset": "target-debug",
|
||||
"targets": [
|
||||
"test_host_uart",
|
||||
"test_hil_opto",
|
||||
"test_<name>"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 4 — Сборка
|
||||
|
||||
```bash
|
||||
# В devcontainer:
|
||||
just build::build-hil
|
||||
|
||||
# Проверить что новый таргет собрался:
|
||||
ls build/target-debug/tests/target/<name>/test_<name>.elf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 5 — `conftest.py`: добавить фикстуры
|
||||
|
||||
Открыть `tools/hil/conftest.py` и добавить в конец раздела с фикстурами загрузки.
|
||||
|
||||
### Базовый тест (без M5)
|
||||
|
||||
```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",
|
||||
)
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def uart_<name>(
|
||||
request: pytest.FixtureRequest,
|
||||
loaded_<name>, # ← гарантирует порядок: ELF раньше UART
|
||||
) -> Generator[serial.Serial, None, None]:
|
||||
ser = _open_uart_and_wait_ready(request)
|
||||
yield ser
|
||||
ser.close()
|
||||
```
|
||||
|
||||
### Тест с M5 (GPIO, реле, питание)
|
||||
|
||||
```python
|
||||
@pytest.fixture(scope="module")
|
||||
def loaded_<name>(request: pytest.FixtureRequest, m5: M5Agent) -> None:
|
||||
"""
|
||||
Зависит от m5 — питание таргета уже включено к моменту загрузки ELF.
|
||||
"""
|
||||
_load_elf(
|
||||
request,
|
||||
Path(cfg.BUILD_DIR) / "tests/target/<name>/test_<name>.elf",
|
||||
)
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def uart_<name>(
|
||||
request: pytest.FixtureRequest,
|
||||
loaded_<name>,
|
||||
) -> Generator[serial.Serial, None, None]:
|
||||
ser = _open_uart_and_wait_ready(request)
|
||||
yield ser
|
||||
ser.close()
|
||||
```
|
||||
|
||||
**Правило:** если тест управляет железом через M5 — `loaded_<name>` должен явно
|
||||
зависеть от `m5`. Это гарантирует что питание включено до того как pyOCD
|
||||
попытается подключиться к MCU.
|
||||
|
||||
---
|
||||
|
||||
## Шаг 6 — `tools/hil/test_<name>.py`
|
||||
|
||||
### Базовый тест (без M5)
|
||||
|
||||
```python
|
||||
"""test_<name>.py — HIL тест <что тестируем>."""
|
||||
import pytest
|
||||
from conftest import uart_cmd
|
||||
|
||||
|
||||
class Test<Name>:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self, uart_<name>):
|
||||
self.ser = uart_<name>
|
||||
|
||||
def test_ping(self):
|
||||
"""Базовая проверка канала."""
|
||||
assert uart_cmd(self.ser, "PING") == "PONG"
|
||||
|
||||
def test_something(self):
|
||||
resp = uart_cmd(self.ser, "MY_CMD")
|
||||
assert resp == "EXPECTED"
|
||||
```
|
||||
|
||||
### Тест с M5
|
||||
|
||||
```python
|
||||
"""test_<name>.py — HIL тест <что тестируем> через M5StampPLC."""
|
||||
import time
|
||||
import pytest
|
||||
from conftest import uart_cmd
|
||||
|
||||
SETTLE_S = 0.15 # ждать после переключения реле
|
||||
|
||||
|
||||
class Test<Name>:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self, uart_<name>, m5):
|
||||
self.ser = uart_<name>
|
||||
self.m5 = m5
|
||||
self.m5.opto_all_off() # или другой сброс состояния стенда
|
||||
time.sleep(SETTLE_S)
|
||||
|
||||
def test_ping(self):
|
||||
assert uart_cmd(self.ser, "PING") == "PONG"
|
||||
|
||||
def test_m5_ping(self):
|
||||
self.m5.ping()
|
||||
|
||||
def test_something_with_relay(self):
|
||||
self.m5.opto_set(1, True)
|
||||
time.sleep(SETTLE_S)
|
||||
assert uart_cmd(self.ser, "READ_INPUT") == "ACTIVE"
|
||||
```
|
||||
|
||||
**Важно про таймауты:** после переключения реле нужно ждать:
|
||||
реле (~10 мс) + оптопара (~0.1 мс) + дебаунс прошивки + один цикл `process()`.
|
||||
Используй активное ожидание вместо фиксированного `sleep` там где важна скорость:
|
||||
|
||||
```python
|
||||
def wait_until(ser, cmd, expected, timeout_s=1.0):
|
||||
deadline = time.monotonic() + timeout_s
|
||||
while time.monotonic() < deadline:
|
||||
if uart_cmd(ser, cmd) == expected:
|
||||
return
|
||||
time.sleep(0.02)
|
||||
raise TimeoutError(f"Ожидали {expected!r} от '{cmd}'")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 7 — `just/host.just`: добавить рецепт (опционально)
|
||||
|
||||
```just
|
||||
[doc('Запустить HIL-тест <name>')]
|
||||
[group('hil')]
|
||||
hil-<name>:
|
||||
HIL_BUILD_DIR={{ _hil_build }} \
|
||||
uv run --directory {{ HIL_DIR }} pytest test_<name>.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Полный цикл
|
||||
|
||||
```bash
|
||||
# 1. devcontainer — собрать прошивку
|
||||
just build::build-hil
|
||||
|
||||
# 2. хост — убедиться что стенд готов (если тест использует M5)
|
||||
just host::m5-deploy # если менялся agent.py
|
||||
just host::m5-scan # убедиться что M5 видна
|
||||
|
||||
# 3. хост — запустить только новый тест
|
||||
just host::hil-<name>
|
||||
|
||||
# 4. хост — загрузить ELF вручную без тестов (для отладки)
|
||||
uv run --directory tools/hil python load_and_run.py \
|
||||
build/target-debug/tests/target/<name>/test_<name>.elf
|
||||
|
||||
# 5. хост — запустить один тест
|
||||
uv run --directory tools/hil pytest test_<name>.py::Test<Name>::test_ping -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Как работают фикстуры
|
||||
|
||||
### Цепочка зависимостей
|
||||
|
||||
```bash
|
||||
test_foo()
|
||||
└── _setup (function scope, autouse)
|
||||
├── uart_<n> (module scope) ← открыт один раз на весь файл
|
||||
│ └── loaded_<n> ← ELF загружен один раз
|
||||
│ └── m5 ← (если нужен) питание включено
|
||||
└── m5 (module scope) ← (если нужен напрямую в тесте)
|
||||
```
|
||||
|
||||
`scope=module` — фикстура создаётся один раз на весь тест-файл, уничтожается
|
||||
после последнего теста. ELF грузится один раз, порт открывается один раз.
|
||||
|
||||
### Порядок при запуске нескольких файлов
|
||||
|
||||
```bash
|
||||
pytest test_uart.py test_<name>.py
|
||||
|
||||
test_uart.py test_<name>.py
|
||||
───────────────────── ─────────────────────
|
||||
loaded_host_uart m5 ← создаётся
|
||||
uart ← создаётся loaded_<name>
|
||||
test_ping uart_<name> ← создаётся
|
||||
test_echo test_ping
|
||||
uart.close() test_something
|
||||
uart_<name>.close()
|
||||
m5 teardown → power(False)
|
||||
```
|
||||
|
||||
Каждый файл — своя загрузка ELF, свой UART-сеанс. MCU перезагружается между файлами.
|
||||
|
||||
---
|
||||
|
||||
## Чеклист
|
||||
|
||||
```bash
|
||||
[ ] tests/target/<name>/main.c — C-прошивка с CLI + READY-паттерн
|
||||
[ ] tests/target/<name>/CMakeLists.txt — сборка с bsp_boot_ram
|
||||
[ ] tests/target/CMakeLists.txt — add_subdirectory(<name>)
|
||||
[ ] CMakePresets.json — добавить test_<name> в targets
|
||||
[ ] tools/hil/conftest.py — loaded_<name> + uart_<name>
|
||||
[ ] tools/hil/test_<name>.py — pytest-тесты
|
||||
[ ] just/host.just — рецепт hil-<name> (опционально)
|
||||
[ ] just build::build-hil — зелёная сборка
|
||||
[ ] just host::hil-<name> — зелёный прогон
|
||||
```
|
||||
204
docs/testing/hil/HIL_HOW_TO.md
Normal file
204
docs/testing/hil/HIL_HOW_TO.md
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
# Как проводить HIL-тесты
|
||||
|
||||
Пошаговый гайд для разработчика. Описывает полный цикл от сборки прошивки
|
||||
до зелёного прогона тестов.
|
||||
|
||||
Детали оборудования и подключений — в [HIL_BENCH.md](HIL_BENCH.md).
|
||||
Как добавить новый тест — в [HIL_CREATE_TEST.md](HIL_CREATE_TEST.md).
|
||||
|
||||
---
|
||||
|
||||
## Шаг 1 — Подготовка окружения (один раз)
|
||||
|
||||
### 1.1 Установить зависимости хоста
|
||||
|
||||
```bash
|
||||
just host::setup-tools
|
||||
```
|
||||
|
||||
Устанавливает Python-зависимости из `tools/hil/uv.lock` (pyOCD, pyserial, pytest
|
||||
и др.). Повторный запуск — no-op если `uv.lock` не изменился.
|
||||
|
||||
### 1.2 Настроить `.env`
|
||||
|
||||
Скопировать `.env.example` в `.env` в корне репозитория и заполнить порты:
|
||||
|
||||
```bash
|
||||
HIL_VCOM_PORT=/dev/cu.usbmodemXXX # MCU-Link VCOM
|
||||
HIL_M5_PORT=/dev/cu.usbmodemYYY # M5StampPLC
|
||||
HIL_BUILD_DIR=build/target-debug # путь к собранным ELF (обычно не менять)
|
||||
```
|
||||
|
||||
Как найти нужные порты:
|
||||
|
||||
```bash
|
||||
just host::m5-scan # покажет M5StampPLC
|
||||
ls /dev/cu.usbmodem* # macOS: все USB CDC устройства
|
||||
ls /dev/ttyACM* # Linux: все USB CDC устройства
|
||||
```
|
||||
|
||||
### 1.3 Задеплоить агент на M5StampPLC
|
||||
|
||||
```bash
|
||||
just host::m5-deploy
|
||||
```
|
||||
|
||||
Копирует `tools/hil/m5/agent.py` на M5 как `/main.py`. Агент запускается
|
||||
автоматически при каждом включении M5.
|
||||
|
||||
> ⚠️ Повторять после **каждого изменения** `agent.py`. Иначе на M5 работает
|
||||
> старая версия и тесты будут падать с неочевидными ошибками.
|
||||
|
||||
Проверить что агент работает:
|
||||
|
||||
```bash
|
||||
just host::m5-cli # интерактивный CLI для ручной отправки команд агенту
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 2 — Физическое подключение стенда
|
||||
|
||||
Убедиться что:
|
||||
|
||||
1. **MCU-Link** подключён к таргету по SWD и к хосту по USB.
|
||||
2. **M5StampPLC** подключён к хосту по USB.
|
||||
3. **RLY1** M5 подключён к VIN таргета — питание управляется программно.
|
||||
4. Сигнальные реле подключены согласно таблице в [HIL_BENCH.md](HIL_BENCH.md).
|
||||
|
||||
Быстрая проверка стенда — включить питание таргета вручную и убедиться что
|
||||
MCU-Link его видит:
|
||||
|
||||
```bash
|
||||
just host::m5-power on
|
||||
|
||||
# В другом терминале — pyOCD должен найти пробник:
|
||||
uv run --directory tools/hil pyocd list
|
||||
|
||||
just host::m5-power off
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 3 — Сборка HIL-прошивок (в devcontainer)
|
||||
|
||||
HIL-прошивки компилируются под ARM и собираются **внутри devcontainer**,
|
||||
потому что там есть `arm-none-eabi-gcc` и весь SDK.
|
||||
|
||||
```bash
|
||||
# Открыть проект в VSCode → Reopen in Container
|
||||
# Затем внутри контейнера:
|
||||
just build::build-hil
|
||||
```
|
||||
|
||||
Что происходит: `cmake --build --preset target-debug-build` собирает все таргеты
|
||||
из `tests/target/*/` и кладёт `.elf` в `build/target-debug/tests/target/`.
|
||||
|
||||
Проверить результат:
|
||||
|
||||
```bash
|
||||
ls build/target-debug/tests/target/
|
||||
# host_uart/test_host_uart.elf
|
||||
# hil_opto/test_hil_opto.elf
|
||||
# ...
|
||||
```
|
||||
|
||||
> Пересобирать нужно только при изменении C-кода прошивок (`tests/target/*/main.c`
|
||||
> или BSP). Изменения в Python-тестах (`tools/hil/test_*.py`) сборки не требуют.
|
||||
|
||||
---
|
||||
|
||||
## Шаг 4 — Запуск тестов (на хосте, вне контейнера)
|
||||
|
||||
### Все HIL-тесты
|
||||
|
||||
```bash
|
||||
just host::hil-run
|
||||
```
|
||||
|
||||
pytest обходит все `test_*.py` в `tools/hil/`, запускает их по очереди.
|
||||
Каждый файл — своя загрузка ELF, свой UART-сеанс, MCU перезагружается между файлами.
|
||||
|
||||
### Конкретный тест
|
||||
|
||||
```bash
|
||||
just host::hil-uart # только test_uart.py
|
||||
just host::hil-opto # только test_opto.py
|
||||
```
|
||||
|
||||
### Один тест-кейс (для отладки)
|
||||
|
||||
```bash
|
||||
uv run --directory tools/hil pytest test_opto.py::TestOptoConnectivity::test_target_ping -v
|
||||
```
|
||||
|
||||
### Без перезагрузки ELF (если прошивка уже запущена)
|
||||
|
||||
```bash
|
||||
uv run --directory tools/hil pytest test_opto.py -v --no-load
|
||||
```
|
||||
|
||||
Удобно при отладке тестов когда прошивка уже в RAM и не нужно каждый раз
|
||||
ждать загрузки через pyOCD.
|
||||
|
||||
---
|
||||
|
||||
## Шаг 5 — Интерпретация результатов
|
||||
|
||||
### Зелёный прогон
|
||||
|
||||
```
|
||||
18 passed in 13.4s
|
||||
```
|
||||
|
||||
### Типичные ошибки и их причины
|
||||
|
||||
**`TimeoutError: Прошивка не отправила READY`**
|
||||
|
||||
Прошивка не запустилась. Возможные причины:
|
||||
- ELF не пересобран после изменений — `just build::build-hil`
|
||||
- MCU не получает питание — `just host::m5-power on`, проверить RLY1
|
||||
- Неверный порт в `.env` — `HIL_VCOM_PORT`
|
||||
- MCU-Link занят GDB-сервером — закрыть `just host::debug-server`
|
||||
|
||||
**`M5 agent не отвечает`**
|
||||
|
||||
- Агент не задеплоен — `just host::m5-deploy`
|
||||
- M5 завис — отключить и подключить USB, повторить деплой
|
||||
- Неверный порт — `HIL_M5_PORT` в `.env`
|
||||
|
||||
**Тест читает INACTIVE вместо ACTIVE (или наоборот)**
|
||||
|
||||
- Провод подключён не к тому реле — сверить [HIL_BENCH.md](HIL_BENCH.md)
|
||||
- `agent.py` изменился но не задеплоен — `just host::m5-deploy`
|
||||
- Слишком маленький `SETTLE_S` — дебаунс прошивки не успел отработать
|
||||
|
||||
**`pyOCD: No connected probes`**
|
||||
|
||||
- MCU-Link не подключён или не виден — проверить USB
|
||||
- На Linux — нет udev-правил: `just host::setup-udev`
|
||||
|
||||
---
|
||||
|
||||
## Ручная отладка
|
||||
|
||||
Если тест падает и непонятно почему — загрузить ELF вручную и пообщаться
|
||||
с прошивкой напрямую:
|
||||
|
||||
```bash
|
||||
# Загрузить ELF без запуска тестов
|
||||
uv run --directory tools/hil python load_and_run.py \
|
||||
build/target-debug/tests/target/hil_opto/test_hil_opto.elf
|
||||
|
||||
# В другом терминале — открыть UART монитор
|
||||
just host::uart-monitor
|
||||
# Теперь можно вводить команды вручную: PING, OPTO_READ 1, ...
|
||||
```
|
||||
|
||||
Управлять стендом вручную через M5:
|
||||
|
||||
```bash
|
||||
just host::m5-cli
|
||||
# {"cmd": "opto_set", "ch": 1, "state": true}
|
||||
# {"ok": true, ...}
|
||||
```
|
||||
|
|
@ -10,28 +10,35 @@
|
|||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static volatile bool is_in1_activated = false;
|
||||
static volatile bool is_in2_activated = false;
|
||||
static volatile bool is_rs_activated = false;
|
||||
static volatile bool g_is_in1_activated = false;
|
||||
static volatile bool g_is_in2_activated = false;
|
||||
static volatile bool g_is_in1_deactivated = false;
|
||||
static volatile bool g_is_in2_deactivated = false;
|
||||
|
||||
static void on_opto_change(bsp_opto_ch_t ch, bsp_opto_state_t state)
|
||||
{
|
||||
if (ch == BSP_OPTO_CH_IN1 && state == BSP_OPTO_STATE_ACTIVE)
|
||||
{
|
||||
/* IN1 активирован */
|
||||
is_in1_activated = true;
|
||||
g_is_in1_activated = true;
|
||||
}
|
||||
|
||||
if (ch == BSP_OPTO_CH_IN2 && state == BSP_OPTO_STATE_ACTIVE)
|
||||
{
|
||||
/* IN2 активирован */
|
||||
is_in2_activated = true;
|
||||
g_is_in2_activated = true;
|
||||
}
|
||||
|
||||
if (ch == BSP_OPTO_CH_RS && state == BSP_OPTO_STATE_ACTIVE)
|
||||
if (ch == BSP_OPTO_CH_IN1 && state == BSP_OPTO_STATE_INACTIVE)
|
||||
{
|
||||
/* RS активирован */
|
||||
is_rs_activated = true;
|
||||
/* IN1 деактивирован */
|
||||
g_is_in1_deactivated = true;
|
||||
}
|
||||
|
||||
if (ch == BSP_OPTO_CH_IN2 && state == BSP_OPTO_STATE_INACTIVE)
|
||||
{
|
||||
/* IN2 деактивирован */
|
||||
g_is_in2_deactivated = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -45,58 +52,44 @@ int main(void)
|
|||
bsp_uart_host_init(UART_BAUDRATE);
|
||||
log_uart_init();
|
||||
|
||||
/* --- Opto init --- */
|
||||
bsp_opto_config_t opto_cfg = {
|
||||
.callbacks = { on_opto_change, on_opto_change, on_opto_change },
|
||||
.callbacks = { on_opto_change, on_opto_change, NULL },
|
||||
.modes = { BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_LEVEL, BSP_OPTO_MODE_LEVEL },
|
||||
.edges = { BSP_OPTO_EDGE_RISING, BSP_OPTO_EDGE_RISING, BSP_OPTO_EDGE_RISING },
|
||||
.rs_as_gpio = true,
|
||||
.debounce_ms = 0, /* instant — для быстрого тестирования */
|
||||
.rs_as_gpio = false,
|
||||
.debounce_ms = DELAY_MS,
|
||||
};
|
||||
bsp_opto_init(&opto_cfg);
|
||||
bsp_led_on(LED_APP);
|
||||
LOG_I("BOOT", "firmware_test started, tick=%lu", (unsigned long) bsp_tick_get_ms());
|
||||
|
||||
bool in1_enable = false;
|
||||
bool in2_enable = false;
|
||||
bool rs_enable = false;
|
||||
while (1)
|
||||
{
|
||||
bsp_opto_process();
|
||||
// Контроль включения
|
||||
if (is_in1_activated)
|
||||
if (g_is_in1_activated)
|
||||
{
|
||||
is_in1_activated = false;
|
||||
in1_enable = true;
|
||||
g_is_in1_activated = false;
|
||||
LOG_D("INPUT", "CH1: ACTIVE!");
|
||||
}
|
||||
if (is_in2_activated)
|
||||
if (g_is_in2_activated)
|
||||
{
|
||||
is_in2_activated = false;
|
||||
in2_enable = true;
|
||||
g_is_in2_activated = false;
|
||||
LOG_D("INPUT", "CH2: ACTIVE!");
|
||||
}
|
||||
if (is_rs_activated)
|
||||
{
|
||||
is_rs_activated = false;
|
||||
rs_enable = true;
|
||||
LOG_D("INPUT", "RS: ACTIVE!");
|
||||
}
|
||||
|
||||
// Контроль выключения
|
||||
if (in1_enable && bsp_opto_read(BSP_OPTO_CH_IN1) == BSP_OPTO_STATE_INACTIVE)
|
||||
if (g_is_in1_deactivated)
|
||||
{
|
||||
in1_enable = false;
|
||||
g_is_in1_deactivated = false;
|
||||
LOG_D("INPUT", "CH1: DISABLED!");
|
||||
}
|
||||
if (in2_enable && bsp_opto_read(BSP_OPTO_CH_IN2) == BSP_OPTO_STATE_INACTIVE)
|
||||
if (g_is_in2_deactivated)
|
||||
{
|
||||
in2_enable = false;
|
||||
g_is_in2_deactivated = false;
|
||||
LOG_D("INPUT", "CH2: DISABLED!");
|
||||
}
|
||||
if (rs_enable && bsp_opto_read(BSP_OPTO_CH_RS) == BSP_OPTO_STATE_INACTIVE)
|
||||
{
|
||||
rs_enable = false;
|
||||
LOG_D("INPUT", "RS: DISABLED!");
|
||||
}
|
||||
|
||||
bsp_delay(DELAY_MS);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,372 +0,0 @@
|
|||
# Добавление нового 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> (опционально)
|
||||
```
|
||||
|
|
@ -80,6 +80,7 @@ uint32_t GPIO_PinRead(GPIO_Type *base, uint32_t pin);
|
|||
void GPIO_SetPinInterruptConfig(GPIO_Type *base, uint32_t pin,
|
||||
gpio_interrupt_mode_t pinInterruptMode);
|
||||
void GPIO_EnableInterrupts(GPIO_Type *base, uint32_t mask);
|
||||
void GPIO_DisableInterrupts(GPIO_Type *base, uint32_t mask);
|
||||
uint32_t GPIO_GetPinsInterruptFlags(GPIO_Type *base);
|
||||
void GPIO_ClearPinsInterruptFlags(GPIO_Type *base, uint32_t mask);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@
|
|||
* BSP_OPTO_CH_IN1 → GPIO1 pin 22
|
||||
* BSP_OPTO_CH_IN2 → GPIO1 pin 21
|
||||
* BSP_OPTO_CH_RS → GPIO1 pin 23
|
||||
*
|
||||
* Полярность: active-HIGH (неинвертирующая оптопара).
|
||||
* raw=1 → BSP_OPTO_STATE_ACTIVE
|
||||
* raw=0 → BSP_OPTO_STATE_INACTIVE
|
||||
*/
|
||||
|
||||
#include "fff.h"
|
||||
|
|
@ -25,6 +29,7 @@ FAKE_VALUE_FUNC(uint32_t, GPIO_PinRead, GPIO_Type *, uint32_t);
|
|||
|
||||
FAKE_VOID_FUNC(GPIO_SetPinInterruptConfig, GPIO_Type *, uint32_t, gpio_interrupt_mode_t);
|
||||
FAKE_VOID_FUNC(GPIO_EnableInterrupts, GPIO_Type *, uint32_t);
|
||||
FAKE_VOID_FUNC(GPIO_DisableInterrupts, GPIO_Type *, uint32_t);
|
||||
FAKE_VALUE_FUNC(uint32_t, GPIO_GetPinsInterruptFlags, GPIO_Type *);
|
||||
FAKE_VOID_FUNC(GPIO_ClearPinsInterruptFlags, GPIO_Type *, uint32_t);
|
||||
|
||||
|
|
@ -50,7 +55,7 @@ FAKE_VALUE_FUNC(uint32_t, bsp_tick_get_ms);
|
|||
|
||||
#define DEBOUNCE_MS 10U
|
||||
|
||||
/* Capture конфигурации GPIO_PinInit (та же техника что в test_bsp_led.c) */
|
||||
/* Capture конфигурации GPIO_PinInit */
|
||||
#define MAX_INIT_CALLS 3U
|
||||
static gpio_pin_config_t s_captured_cfg[MAX_INIT_CALLS];
|
||||
static uint32_t s_captured_pin[MAX_INIT_CALLS];
|
||||
|
|
@ -67,8 +72,8 @@ static void GPIO_PinInit_capture(GPIO_Type *base, uint32_t pin, const gpio_pin_c
|
|||
}
|
||||
}
|
||||
|
||||
/* Capture SetPinInterruptConfig */
|
||||
#define MAX_IRQ_CONFIG_CALLS 3U
|
||||
/* Capture SetPinInterruptConfig — увеличен буфер: при MODE_LEVEL ISR меняет фронт */
|
||||
#define MAX_IRQ_CONFIG_CALLS 8U
|
||||
static gpio_interrupt_mode_t s_captured_irq_mode[MAX_IRQ_CONFIG_CALLS];
|
||||
static uint32_t s_captured_irq_pin[MAX_IRQ_CONFIG_CALLS];
|
||||
static uint32_t s_irq_config_idx;
|
||||
|
|
@ -97,11 +102,20 @@ static void test_callback(bsp_opto_ch_t ch, bsp_opto_state_t state)
|
|||
s_cb_count++;
|
||||
}
|
||||
|
||||
/* Конфигурация по умолчанию (rs_as_gpio = false, 2 канала) */
|
||||
/*
|
||||
* Конфигурация по умолчанию:
|
||||
* IN1, IN2 — MODE_LEVEL
|
||||
* RS — отключён (rs_as_gpio = false)
|
||||
*/
|
||||
static bsp_opto_config_t make_default_cfg(void)
|
||||
{
|
||||
bsp_opto_config_t cfg = {
|
||||
.callbacks = { test_callback, test_callback, NULL },
|
||||
.modes = {
|
||||
[BSP_OPTO_CH_IN1] = BSP_OPTO_MODE_LEVEL,
|
||||
[BSP_OPTO_CH_IN2] = BSP_OPTO_MODE_LEVEL,
|
||||
[BSP_OPTO_CH_RS] = BSP_OPTO_MODE_LEVEL,
|
||||
},
|
||||
.edges = {
|
||||
[BSP_OPTO_CH_IN1] = BSP_OPTO_EDGE_RISING,
|
||||
[BSP_OPTO_CH_IN2] = BSP_OPTO_EDGE_RISING,
|
||||
|
|
@ -133,6 +147,7 @@ void setUp(void)
|
|||
RESET_FAKE(GPIO_PinRead);
|
||||
RESET_FAKE(GPIO_SetPinInterruptConfig);
|
||||
RESET_FAKE(GPIO_EnableInterrupts);
|
||||
RESET_FAKE(GPIO_DisableInterrupts);
|
||||
RESET_FAKE(GPIO_GetPinsInterruptFlags);
|
||||
RESET_FAKE(GPIO_ClearPinsInterruptFlags);
|
||||
RESET_FAKE(EnableIRQ);
|
||||
|
|
@ -147,8 +162,11 @@ void setUp(void)
|
|||
GPIO_PinInit_fake.custom_fake = GPIO_PinInit_capture;
|
||||
GPIO_SetPinInterruptConfig_fake.custom_fake = GPIO_SetPinInterruptConfig_capture;
|
||||
|
||||
/* По умолчанию пины INACTIVE (active-low → raw = 1) */
|
||||
GPIO_PinRead_fake.return_val = 1U;
|
||||
/*
|
||||
* По умолчанию пины INACTIVE.
|
||||
* active-HIGH: raw=0 → INACTIVE.
|
||||
*/
|
||||
GPIO_PinRead_fake.return_val = 0U;
|
||||
}
|
||||
|
||||
void tearDown(void)
|
||||
|
|
@ -232,15 +250,19 @@ void test_init_rs_as_gpio_false_no_board_init(void)
|
|||
TEST_ASSERT_EQUAL(0U, BOARD_InitRS_GPIO_fake.call_count);
|
||||
}
|
||||
|
||||
/* ── Тесты: edge config ──────────────────────────────────────────────────── */
|
||||
/* ── Тесты: начальный фронт MODE_LEVEL ──────────────────────────────────── */
|
||||
|
||||
void test_init_rising_edge_sets_rising_irq_mode(void)
|
||||
/*
|
||||
* При init пин LOW (raw=0, INACTIVE) → ожидаем RISING фронт
|
||||
* (сигнал ещё не пришёл, ждём его появления).
|
||||
*/
|
||||
void test_init_level_pin_low_selects_rising_edge(void)
|
||||
{
|
||||
GPIO_PinRead_fake.return_val = 0U; /* INACTIVE */
|
||||
bsp_opto_config_t cfg = make_default_cfg();
|
||||
cfg.edges[BSP_OPTO_CH_IN1] = BSP_OPTO_EDGE_RISING;
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
/* Найти запись для пина IN1 */
|
||||
/* Найти первую запись для IN1 */
|
||||
uint32_t idx = 0U;
|
||||
for (uint32_t i = 0U; i < s_irq_config_idx; i++)
|
||||
{
|
||||
|
|
@ -253,10 +275,14 @@ void test_init_rising_edge_sets_rising_irq_mode(void)
|
|||
TEST_ASSERT_EQUAL(kGPIO_IntRisingEdge, s_captured_irq_mode[idx]);
|
||||
}
|
||||
|
||||
void test_init_falling_edge_sets_falling_irq_mode(void)
|
||||
/*
|
||||
* При init пин HIGH (raw=1, ACTIVE) → ожидаем FALLING фронт
|
||||
* (сигнал уже активен, ждём его снятия).
|
||||
*/
|
||||
void test_init_level_pin_high_selects_falling_edge(void)
|
||||
{
|
||||
GPIO_PinRead_fake.return_val = 1U; /* ACTIVE */
|
||||
bsp_opto_config_t cfg = make_default_cfg();
|
||||
cfg.edges[BSP_OPTO_CH_IN1] = BSP_OPTO_EDGE_FALLING;
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
uint32_t idx = 0U;
|
||||
|
|
@ -271,12 +297,52 @@ void test_init_falling_edge_sets_falling_irq_mode(void)
|
|||
TEST_ASSERT_EQUAL(kGPIO_IntFallingEdge, s_captured_irq_mode[idx]);
|
||||
}
|
||||
|
||||
/* ── Тесты: переключение фронта в ISR (MODE_LEVEL) ──────────────────────── */
|
||||
|
||||
/*
|
||||
* После RISING фронта ISR должен переключить направление на FALLING,
|
||||
* чтобы поймать возврат сигнала.
|
||||
*/
|
||||
void test_isr_level_toggles_edge_after_rising(void)
|
||||
{
|
||||
GPIO_PinRead_fake.return_val = 0U; /* init: pin LOW → начинаем с RISING */
|
||||
bsp_opto_config_t cfg = make_default_cfg();
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
uint32_t irq_calls_after_init = s_irq_config_idx;
|
||||
|
||||
/* ISR: RISING фронт (pin стал HIGH) */
|
||||
simulate_isr(OPTO_IN1_PIN, 1U);
|
||||
|
||||
/* Должна появиться новая запись SetPinInterruptConfig с FALLING */
|
||||
TEST_ASSERT_GREATER_THAN(irq_calls_after_init, s_irq_config_idx);
|
||||
TEST_ASSERT_EQUAL(kGPIO_IntFallingEdge, s_captured_irq_mode[s_irq_config_idx - 1U]);
|
||||
}
|
||||
|
||||
/*
|
||||
* После FALLING фронта ISR должен переключить направление обратно на RISING.
|
||||
*/
|
||||
void test_isr_level_toggles_edge_after_falling(void)
|
||||
{
|
||||
GPIO_PinRead_fake.return_val = 1U; /* init: pin HIGH → начинаем с FALLING */
|
||||
bsp_opto_config_t cfg = make_default_cfg();
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
uint32_t irq_calls_after_init = s_irq_config_idx;
|
||||
|
||||
/* ISR: FALLING фронт (pin стал LOW) */
|
||||
simulate_isr(OPTO_IN1_PIN, 0U);
|
||||
|
||||
TEST_ASSERT_GREATER_THAN(irq_calls_after_init, s_irq_config_idx);
|
||||
TEST_ASSERT_EQUAL(kGPIO_IntRisingEdge, s_captured_irq_mode[s_irq_config_idx - 1U]);
|
||||
}
|
||||
|
||||
/* ── Тесты: bsp_opto_read — начальное состояние ─────────────────────────── */
|
||||
|
||||
void test_read_initial_inactive_when_pin_high(void)
|
||||
void test_read_initial_inactive_when_pin_low(void)
|
||||
{
|
||||
/* active-low: raw=1 → INACTIVE */
|
||||
GPIO_PinRead_fake.return_val = 1U;
|
||||
/* active-HIGH: raw=0 → INACTIVE */
|
||||
GPIO_PinRead_fake.return_val = 0U;
|
||||
bsp_opto_config_t cfg = make_default_cfg();
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
|
|
@ -284,10 +350,10 @@ void test_read_initial_inactive_when_pin_high(void)
|
|||
TEST_ASSERT_EQUAL(BSP_OPTO_STATE_INACTIVE, bsp_opto_read(BSP_OPTO_CH_IN2));
|
||||
}
|
||||
|
||||
void test_read_initial_active_when_pin_low(void)
|
||||
void test_read_initial_active_when_pin_high(void)
|
||||
{
|
||||
/* active-low: raw=0 → ACTIVE */
|
||||
GPIO_PinRead_fake.return_val = 0U;
|
||||
/* active-HIGH: raw=1 → ACTIVE */
|
||||
GPIO_PinRead_fake.return_val = 1U;
|
||||
bsp_opto_config_t cfg = make_default_cfg();
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
|
|
@ -308,20 +374,20 @@ void test_read_invalid_channel_returns_inactive(void)
|
|||
TEST_ASSERT_EQUAL(BSP_OPTO_STATE_INACTIVE, bsp_opto_read((bsp_opto_ch_t) BSP_OPTO_CH_COUNT));
|
||||
}
|
||||
|
||||
/* ── Тесты: bsp_opto_process — дебаунс ──────────────────────────────────── */
|
||||
/* ── Тесты: bsp_opto_process — дебаунс (MODE_LEVEL) ─────────────────────── */
|
||||
|
||||
void test_process_before_debounce_no_callback(void)
|
||||
{
|
||||
bsp_opto_config_t cfg = make_default_cfg();
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
/* ISR фиксирует фронт в момент T=100 */
|
||||
/* ISR: RISING фронт в T=100 (pin стал HIGH = ACTIVE) */
|
||||
bsp_tick_get_ms_fake.return_val = 100U;
|
||||
simulate_isr(OPTO_IN1_PIN, 0U); /* pin → ACTIVE */
|
||||
simulate_isr(OPTO_IN1_PIN, 1U);
|
||||
|
||||
/* process в T=105: прошло 5ms < debounce_ms(10) */
|
||||
bsp_tick_get_ms_fake.return_val = 105U;
|
||||
GPIO_PinRead_fake.return_val = 0U;
|
||||
GPIO_PinRead_fake.return_val = 1U;
|
||||
bsp_opto_process();
|
||||
|
||||
TEST_ASSERT_EQUAL(0U, s_cb_count);
|
||||
|
|
@ -333,11 +399,11 @@ void test_process_after_debounce_fires_callback(void)
|
|||
bsp_opto_init(&cfg);
|
||||
|
||||
bsp_tick_get_ms_fake.return_val = 100U;
|
||||
simulate_isr(OPTO_IN1_PIN, 0U); /* pin → ACTIVE */
|
||||
simulate_isr(OPTO_IN1_PIN, 1U); /* RISING → ACTIVE */
|
||||
|
||||
/* process в T=112: прошло 12ms >= debounce_ms(10) */
|
||||
bsp_tick_get_ms_fake.return_val = 112U;
|
||||
GPIO_PinRead_fake.return_val = 0U;
|
||||
GPIO_PinRead_fake.return_val = 1U;
|
||||
bsp_opto_process();
|
||||
|
||||
TEST_ASSERT_EQUAL(1U, s_cb_count);
|
||||
|
|
@ -349,10 +415,10 @@ void test_process_callback_receives_correct_channel(void)
|
|||
bsp_opto_init(&cfg);
|
||||
|
||||
bsp_tick_get_ms_fake.return_val = 0U;
|
||||
simulate_isr(OPTO_IN1_PIN, 0U);
|
||||
simulate_isr(OPTO_IN1_PIN, 1U);
|
||||
|
||||
bsp_tick_get_ms_fake.return_val = DEBOUNCE_MS + 1U;
|
||||
GPIO_PinRead_fake.return_val = 0U;
|
||||
GPIO_PinRead_fake.return_val = 1U;
|
||||
bsp_opto_process();
|
||||
|
||||
TEST_ASSERT_EQUAL(BSP_OPTO_CH_IN1, s_cb_ch);
|
||||
|
|
@ -364,10 +430,10 @@ void test_process_callback_receives_active_state(void)
|
|||
bsp_opto_init(&cfg);
|
||||
|
||||
bsp_tick_get_ms_fake.return_val = 0U;
|
||||
simulate_isr(OPTO_IN1_PIN, 0U); /* raw=0 → ACTIVE */
|
||||
simulate_isr(OPTO_IN1_PIN, 1U); /* raw=1 → ACTIVE */
|
||||
|
||||
bsp_tick_get_ms_fake.return_val = DEBOUNCE_MS + 1U;
|
||||
GPIO_PinRead_fake.return_val = 0U;
|
||||
GPIO_PinRead_fake.return_val = 1U;
|
||||
bsp_opto_process();
|
||||
|
||||
TEST_ASSERT_EQUAL(BSP_OPTO_STATE_ACTIVE, s_cb_state);
|
||||
|
|
@ -375,17 +441,17 @@ void test_process_callback_receives_active_state(void)
|
|||
|
||||
void test_process_callback_receives_inactive_state(void)
|
||||
{
|
||||
/* Начинаем с ACTIVE — init с pin=0 */
|
||||
GPIO_PinRead_fake.return_val = 0U;
|
||||
/* Начинаем с ACTIVE: init с pin HIGH */
|
||||
GPIO_PinRead_fake.return_val = 1U;
|
||||
bsp_opto_config_t cfg = make_default_cfg();
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
/* Фронт: pin → INACTIVE (raw=1) */
|
||||
/* FALLING фронт: pin стал LOW → INACTIVE */
|
||||
bsp_tick_get_ms_fake.return_val = 0U;
|
||||
simulate_isr(OPTO_IN1_PIN, 1U);
|
||||
simulate_isr(OPTO_IN1_PIN, 0U);
|
||||
|
||||
bsp_tick_get_ms_fake.return_val = DEBOUNCE_MS + 1U;
|
||||
GPIO_PinRead_fake.return_val = 1U;
|
||||
GPIO_PinRead_fake.return_val = 0U;
|
||||
bsp_opto_process();
|
||||
|
||||
TEST_ASSERT_EQUAL(BSP_OPTO_STATE_INACTIVE, s_cb_state);
|
||||
|
|
@ -393,17 +459,17 @@ void test_process_callback_receives_inactive_state(void)
|
|||
|
||||
void test_process_no_callback_if_state_unchanged(void)
|
||||
{
|
||||
/* Init с pin=0 → ACTIVE */
|
||||
GPIO_PinRead_fake.return_val = 0U;
|
||||
/* Init с pin HIGH → ACTIVE */
|
||||
GPIO_PinRead_fake.return_val = 1U;
|
||||
bsp_opto_config_t cfg = make_default_cfg();
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
/* ISR срабатывает, но пин при перечитке всё ещё ACTIVE */
|
||||
bsp_tick_get_ms_fake.return_val = 0U;
|
||||
simulate_isr(OPTO_IN1_PIN, 0U);
|
||||
simulate_isr(OPTO_IN1_PIN, 1U);
|
||||
|
||||
bsp_tick_get_ms_fake.return_val = DEBOUNCE_MS + 1U;
|
||||
GPIO_PinRead_fake.return_val = 0U; /* состояние не изменилось */
|
||||
GPIO_PinRead_fake.return_val = 1U; /* состояние не изменилось */
|
||||
bsp_opto_process();
|
||||
|
||||
TEST_ASSERT_EQUAL(0U, s_cb_count);
|
||||
|
|
@ -415,10 +481,10 @@ void test_process_pending_cleared_after_debounce(void)
|
|||
bsp_opto_init(&cfg);
|
||||
|
||||
bsp_tick_get_ms_fake.return_val = 0U;
|
||||
simulate_isr(OPTO_IN1_PIN, 0U);
|
||||
simulate_isr(OPTO_IN1_PIN, 1U);
|
||||
|
||||
bsp_tick_get_ms_fake.return_val = DEBOUNCE_MS + 1U;
|
||||
GPIO_PinRead_fake.return_val = 0U;
|
||||
GPIO_PinRead_fake.return_val = 1U;
|
||||
bsp_opto_process();
|
||||
|
||||
/* Второй вызов process без нового ISR — коллбэк не должен стрелять снова */
|
||||
|
|
@ -436,18 +502,149 @@ void test_channels_are_independent(void)
|
|||
|
||||
/* ISR только для IN1 */
|
||||
bsp_tick_get_ms_fake.return_val = 0U;
|
||||
simulate_isr(OPTO_IN1_PIN, 0U);
|
||||
simulate_isr(OPTO_IN1_PIN, 1U);
|
||||
|
||||
bsp_tick_get_ms_fake.return_val = DEBOUNCE_MS + 1U;
|
||||
GPIO_PinRead_fake.return_val = 0U;
|
||||
GPIO_PinRead_fake.return_val = 1U;
|
||||
bsp_opto_process();
|
||||
|
||||
/* IN1 изменился, IN2 — нет */
|
||||
/* IN1 изменился (INACTIVE→ACTIVE), IN2 — нет */
|
||||
TEST_ASSERT_EQUAL(BSP_OPTO_STATE_ACTIVE, bsp_opto_read(BSP_OPTO_CH_IN1));
|
||||
TEST_ASSERT_EQUAL(BSP_OPTO_STATE_INACTIVE, bsp_opto_read(BSP_OPTO_CH_IN2));
|
||||
TEST_ASSERT_EQUAL(1U, s_cb_count); /* ровно один коллбэк */
|
||||
}
|
||||
|
||||
/* ── Тесты: MODE_PROTO ───────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Вспомогательная конфигурация: IN1/IN2 — LEVEL, RS — PROTO.
|
||||
*/
|
||||
static bsp_opto_config_t make_proto_cfg(void)
|
||||
{
|
||||
bsp_opto_config_t cfg = {
|
||||
.callbacks = { test_callback, test_callback, test_callback },
|
||||
.modes = {
|
||||
[BSP_OPTO_CH_IN1] = BSP_OPTO_MODE_LEVEL,
|
||||
[BSP_OPTO_CH_IN2] = BSP_OPTO_MODE_LEVEL,
|
||||
[BSP_OPTO_CH_RS] = BSP_OPTO_MODE_PROTO,
|
||||
},
|
||||
.edges = {
|
||||
[BSP_OPTO_CH_IN1] = BSP_OPTO_EDGE_RISING,
|
||||
[BSP_OPTO_CH_IN2] = BSP_OPTO_EDGE_RISING,
|
||||
[BSP_OPTO_CH_RS] = BSP_OPTO_EDGE_RISING,
|
||||
},
|
||||
.rs_as_gpio = true,
|
||||
.debounce_ms = DEBOUNCE_MS,
|
||||
};
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/*
|
||||
* ISR для MODE_PROTO должен немедленно вызвать коллбэк,
|
||||
* не дожидаясь bsp_opto_process().
|
||||
*/
|
||||
void test_proto_isr_fires_callback_immediately(void)
|
||||
{
|
||||
bsp_opto_config_t cfg = make_proto_cfg();
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
/* ISR: RISING фронт на RS */
|
||||
simulate_isr(OPTO_RS_PIN, 1U);
|
||||
|
||||
/* Коллбэк должен уже сработать — process ещё не вызывался */
|
||||
TEST_ASSERT_EQUAL(1U, s_cb_count);
|
||||
TEST_ASSERT_EQUAL(BSP_OPTO_CH_RS, s_cb_ch);
|
||||
TEST_ASSERT_EQUAL(BSP_OPTO_STATE_ACTIVE, s_cb_state);
|
||||
}
|
||||
|
||||
/*
|
||||
* После ISR прерывание RS должно быть отключено
|
||||
* (GPIO_DisableInterrupts вызван с маской 1<<23).
|
||||
*/
|
||||
void test_proto_isr_disables_irq_after_callback(void)
|
||||
{
|
||||
bsp_opto_config_t cfg = make_proto_cfg();
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
simulate_isr(OPTO_RS_PIN, 1U);
|
||||
|
||||
TEST_ASSERT_EQUAL(1U, GPIO_DisableInterrupts_fake.call_count);
|
||||
TEST_ASSERT_EQUAL(1UL << OPTO_RS_PIN, GPIO_DisableInterrupts_fake.arg1_val);
|
||||
}
|
||||
|
||||
/*
|
||||
* bsp_opto_process() НЕ должен вызывать коллбэк для MODE_PROTO каналов —
|
||||
* они обрабатываются в ISR.
|
||||
*/
|
||||
void test_proto_process_does_not_fire_callback(void)
|
||||
{
|
||||
bsp_opto_config_t cfg = make_proto_cfg();
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
/* Сбрасываем счётчик после ISR */
|
||||
simulate_isr(OPTO_RS_PIN, 1U);
|
||||
s_cb_count = 0U;
|
||||
|
||||
/* process не должен добавить ещё один вызов */
|
||||
bsp_tick_get_ms_fake.return_val = DEBOUNCE_MS + 1U;
|
||||
bsp_opto_process();
|
||||
|
||||
TEST_ASSERT_EQUAL(0U, s_cb_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* bsp_opto_proto_arm() должен перевзвести прерывание RS:
|
||||
* вызвать GPIO_ClearPinsInterruptFlags + GPIO_EnableInterrupts
|
||||
* + GPIO_SetPinInterruptConfig для нужного пина.
|
||||
*/
|
||||
void test_proto_arm_reenables_irq(void)
|
||||
{
|
||||
bsp_opto_config_t cfg = make_proto_cfg();
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
simulate_isr(OPTO_RS_PIN, 1U);
|
||||
|
||||
/* Сбрасываем счётчики — смотрим только на arm */
|
||||
RESET_FAKE(GPIO_EnableInterrupts);
|
||||
RESET_FAKE(GPIO_ClearPinsInterruptFlags);
|
||||
|
||||
bsp_opto_proto_arm(BSP_OPTO_CH_RS);
|
||||
|
||||
TEST_ASSERT_EQUAL(1U, GPIO_EnableInterrupts_fake.call_count);
|
||||
TEST_ASSERT_EQUAL(1U, GPIO_ClearPinsInterruptFlags_fake.call_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* bsp_opto_proto_arm() для канала MODE_LEVEL — no-op,
|
||||
* не должен трогать прерывания.
|
||||
*/
|
||||
void test_proto_arm_noop_for_level_channel(void)
|
||||
{
|
||||
bsp_opto_config_t cfg = make_default_cfg();
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
RESET_FAKE(GPIO_EnableInterrupts);
|
||||
RESET_FAKE(GPIO_ClearPinsInterruptFlags);
|
||||
|
||||
bsp_opto_proto_arm(BSP_OPTO_CH_IN1);
|
||||
|
||||
TEST_ASSERT_EQUAL(0U, GPIO_EnableInterrupts_fake.call_count);
|
||||
TEST_ASSERT_EQUAL(0U, GPIO_ClearPinsInterruptFlags_fake.call_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* bsp_opto_read() для канала MODE_PROTO всегда возвращает INACTIVE —
|
||||
* состояние RS отслеживается коллбэком из ISR.
|
||||
*/
|
||||
void test_proto_read_always_returns_inactive(void)
|
||||
{
|
||||
GPIO_PinRead_fake.return_val = 1U; /* пин HIGH */
|
||||
bsp_opto_config_t cfg = make_proto_cfg();
|
||||
bsp_opto_init(&cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL(BSP_OPTO_STATE_INACTIVE, bsp_opto_read(BSP_OPTO_CH_RS));
|
||||
}
|
||||
|
||||
/* ── main ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
int main(void)
|
||||
|
|
@ -466,13 +663,17 @@ int main(void)
|
|||
RUN_TEST(test_init_rs_as_gpio_calls_board_init);
|
||||
RUN_TEST(test_init_rs_as_gpio_false_no_board_init);
|
||||
|
||||
/* edge config */
|
||||
RUN_TEST(test_init_rising_edge_sets_rising_irq_mode);
|
||||
RUN_TEST(test_init_falling_edge_sets_falling_irq_mode);
|
||||
/* начальный фронт MODE_LEVEL */
|
||||
RUN_TEST(test_init_level_pin_low_selects_rising_edge);
|
||||
RUN_TEST(test_init_level_pin_high_selects_falling_edge);
|
||||
|
||||
/* переключение фронта в ISR */
|
||||
RUN_TEST(test_isr_level_toggles_edge_after_rising);
|
||||
RUN_TEST(test_isr_level_toggles_edge_after_falling);
|
||||
|
||||
/* read — начальное состояние */
|
||||
RUN_TEST(test_read_initial_inactive_when_pin_high);
|
||||
RUN_TEST(test_read_initial_active_when_pin_low);
|
||||
RUN_TEST(test_read_initial_inactive_when_pin_low);
|
||||
RUN_TEST(test_read_initial_active_when_pin_high);
|
||||
RUN_TEST(test_read_disabled_rs_channel_returns_inactive);
|
||||
RUN_TEST(test_read_invalid_channel_returns_inactive);
|
||||
|
||||
|
|
@ -488,5 +689,13 @@ int main(void)
|
|||
/* независимость каналов */
|
||||
RUN_TEST(test_channels_are_independent);
|
||||
|
||||
/* MODE_PROTO */
|
||||
RUN_TEST(test_proto_isr_fires_callback_immediately);
|
||||
RUN_TEST(test_proto_isr_disables_irq_after_callback);
|
||||
RUN_TEST(test_proto_process_does_not_fire_callback);
|
||||
RUN_TEST(test_proto_arm_reenables_irq);
|
||||
RUN_TEST(test_proto_arm_noop_for_level_channel);
|
||||
RUN_TEST(test_proto_read_always_returns_inactive);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
# HIL Testing Toolchain — MIMXRT1052
|
||||
|
||||
> Документ описывает архитектуру и инструментальный стек для Hardware-in-the-Loop (HIL) тестирования на базе NXP MIMXRT1052. Тесты запускаются на рабочих станциях разработчиков, оркестрация — из DevContainer.
|
||||
|
||||
---
|
||||
|
||||
## Оборудование
|
||||
|
||||
| Компонент | Роль |
|
||||
| :--- | :--- |
|
||||
| NXP MCU-Link | Отладчик + VCOM-мост (один USB-кабель) |
|
||||
| MIMXRT1052 (таргет) | Целевое устройство |
|
||||
|
||||
MCU-Link предоставляет два логических канала по одному USB:
|
||||
|
||||
- **CMSIS-DAP** — прошивка и сброс таргета (control plane)
|
||||
- **VCOM (USB-UART)** — тестовый вывод с таргета (data plane)
|
||||
|
||||
---
|
||||
|
||||
## Инструментальный стек
|
||||
|
||||
| Задача | Инструмент |
|
||||
| :--- | :--- |
|
||||
| Прошивка и сброс таргета | `pyocd` Python API |
|
||||
| Проброс зонда в DevContainer | `pyocd server --allow-remote` по TCP |
|
||||
| Чтение тестового вывода | `pyserial` (VCOM) или `socat` TCP-мост |
|
||||
| Оркестрация HIL-тестов | `pytest` + фикстуры в `conftest.py` |
|
||||
| Сборка C-кода тестовой прошивки | `CMake` (вызывается из pytest fixture) |
|
||||
| Тестовый фреймворк в прошивке | Unity + fff |
|
||||
| Unit-тесты (без железа) | CMake + `CTest` (в DevContainer, без таргета) |
|
||||
| Интерактивная отладка | `pyocd gdbserver` + VS Code **cortex-debug** |
|
||||
|
|
@ -139,12 +139,21 @@ int main(void)
|
|||
bsp_led_init();
|
||||
bsp_uart_host_init(CLI_BAUD_RATE);
|
||||
|
||||
/* --- Opto init --- */
|
||||
/* --- Opto init: все три канала MODE_LEVEL --- */
|
||||
bsp_opto_config_t opto_cfg = {
|
||||
.callbacks = { opto_callback, opto_callback, opto_callback },
|
||||
.edges = { BSP_OPTO_EDGE_RISING, BSP_OPTO_EDGE_RISING, BSP_OPTO_EDGE_RISING },
|
||||
.modes = {
|
||||
[BSP_OPTO_CH_IN1] = BSP_OPTO_MODE_LEVEL,
|
||||
[BSP_OPTO_CH_IN2] = BSP_OPTO_MODE_LEVEL,
|
||||
[BSP_OPTO_CH_RS] = BSP_OPTO_MODE_LEVEL,
|
||||
},
|
||||
.edges = {
|
||||
[BSP_OPTO_CH_IN1] = BSP_OPTO_EDGE_RISING,
|
||||
[BSP_OPTO_CH_IN2] = BSP_OPTO_EDGE_RISING,
|
||||
[BSP_OPTO_CH_RS] = BSP_OPTO_EDGE_RISING,
|
||||
},
|
||||
.rs_as_gpio = true,
|
||||
.debounce_ms = 10U, /* instant — для быстрого тестирования */
|
||||
.debounce_ms = 10U,
|
||||
};
|
||||
bsp_opto_init(&opto_cfg);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
# tools/hil — HIL-тесты и утилиты отладки MIMXRT1052
|
||||
# tools/hil
|
||||
|
||||
Изолированное Python-окружение на базе [uv](https://docs.astral.sh/uv/) для:
|
||||
- **HIL-тестов** (Hardware-in-the-Loop) — загрузка ELF в RAM и pytest через UART
|
||||
- **GDB-сервера** для отладки из VSCode (pyocd gdbserver)
|
||||
- **SWD-прошивки** через `flash_swd.py` (вызывается из `tools/host/`)
|
||||
Python-окружение на базе [uv](https://docs.astral.sh/uv/) для HIL-тестов,
|
||||
GDB-сервера отладки и SWD-прошивки через `flash_swd.py`.
|
||||
|
||||
Запускается на **хост-машине** — не внутри devcontainer.
|
||||
|
||||
|
|
@ -11,308 +9,58 @@
|
|||
|
||||
## Структура
|
||||
|
||||
```
|
||||
```bash
|
||||
tools/hil/
|
||||
├── conftest.py — pytest-фикстуры: загрузка ELF, открытие UART, ожидание READY
|
||||
├── env_config.py — конфигурация из переменных окружения (.env → just → pytest)
|
||||
├── pyocd_utils.py — утилиты pyOCD: FLEXRAM, ELF-загрузка, запуск из векторов
|
||||
├── load_and_run.py — CLI-обёртка: загрузить ELF в RAM и запустить вручную
|
||||
├── test_uart.py — HIL-тесты bsp_uart_host (PING/ECHO/BUF_SIZE)
|
||||
├── pyproject.toml — зависимости (pyocd, pyserial, pytest)
|
||||
└── uv.lock — lockfile (коммитить)
|
||||
├── conftest.py — pytest-фикстуры (m5, loaded_<n>, uart_<n>)
|
||||
├── env_config.py — конфигурация из os.environ / .env
|
||||
├── pyocd_utils.py — FLEXRAM init, ELF loader, run_from_vectors
|
||||
├── load_and_run.py — CLI-утилита: загрузить ELF в RAM вручную
|
||||
├── test_uart.py — HIL тест bsp_uart_host (без стенда)
|
||||
├── test_opto.py — HIL тест bsp_opto (через M5StampPLC)
|
||||
├── m5/
|
||||
│ ├── agent.py — MicroPython агент на M5StampPLC (реле, входы, CAN)
|
||||
│ ├── cli.py — интерактивный CLI для ручного тестирования стенда и таргета
|
||||
│ ├── power.py — управление питанием таргета (RLY1) из командной строки
|
||||
│ └── firmware/ — прошивки MicroPython для M5StampPLC
|
||||
│ ├── v1.25/ — MicroPython 1.25 — используется (поддерживает CAN)
|
||||
│ └── v1.27/ — MicroPython 1.27 — CAN не поддерживается, не использовать
|
||||
├── pyproject.toml
|
||||
└── uv.lock
|
||||
```
|
||||
|
||||
> **MicroPython на M5StampPLC:** использовать прошивку из `m5/firmware/v1.25/`.
|
||||
> В v1.27 модуль CAN недоступен — `agent.py` инициализирует CAN при старте,
|
||||
> тесты с CAN не пройдут. Загрузить прошивку можно с помощью [утилиты](https://github.com/esp-rs/espflash)
|
||||
|
||||
---
|
||||
|
||||
## Концепция HIL-тестов
|
||||
## Документация
|
||||
|
||||
```bash
|
||||
espflash write-bin 0 ESP32_GENERIC_S3-20251209-v1.27.0.bin # microPython
|
||||
Полное описание стека HIL-тестирования — в `docs/testing/hil/`:
|
||||
|
||||
```
|
||||
|
||||
Каждый HIL-тест — это пара:
|
||||
|
||||
```
|
||||
tests/target/<name>/main.c ← C-прошивка с текстовым CLI через UART
|
||||
tools/hil/test_<name>.py ← pytest-тесты, общаются с прошивкой по UART
|
||||
```
|
||||
|
||||
pyOCD загружает `.elf` в RAM через MCU-Link (CMSIS-DAP). pytest общается с
|
||||
прошивкой через MCU-Link VCOM (pyserial):
|
||||
|
||||
```
|
||||
pytest → uart_cmd("PING\r\n") → MCU-Link VCOM → RT1052 → "PONG\r\n" → pytest
|
||||
```
|
||||
|
||||
ELF загружается в **ITCM/DTCM** (не Flash) — быстро, не изнашивает Flash,
|
||||
не требует HAB-образа.
|
||||
| Документ | Содержимое |
|
||||
|----------|-----------|
|
||||
| [HIL_HOWTO.md](../../docs/testing/hil/HIL_HOWTO.md) | Как запускать HIL-тесты (пошагово) |
|
||||
| [HIL_BENCH.md](../../docs/testing/hil/HIL_BENCH.md) | Стенд: оборудование, подключение, маппинг реле |
|
||||
| [HIL_CREATE_TEST.md](../../docs/testing/hil/HIL_CREATE_TEST.md) | Как добавить новый HIL-тест |
|
||||
|
||||
---
|
||||
|
||||
## Предварительные требования
|
||||
|
||||
### 1. uv — один раз на машину
|
||||
## Быстрый старт
|
||||
|
||||
```bash
|
||||
# macOS / Linux
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
# Первый раз: установить зависимости
|
||||
cd tools/hil && uv sync
|
||||
|
||||
# Windows
|
||||
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
|
||||
# Заполнить .env в корне репозитория (порты MCU-Link и M5)
|
||||
# Задеплоить агент на M5 (после изменений agent.py — повторить)
|
||||
just host::m5-deploy
|
||||
|
||||
# devcontainer: собрать HIL ELF
|
||||
just build::build-hil
|
||||
|
||||
# хост: запустить тесты
|
||||
just host::hil-run
|
||||
just host::hil-uart # только UART тесты
|
||||
just host::hil-opto # только opto тесты
|
||||
```
|
||||
|
||||
### 2. Зависимости проекта — один раз
|
||||
|
||||
```bash
|
||||
cd tools/hil
|
||||
uv sync
|
||||
```
|
||||
|
||||
### 3. Настроить `.env` в корне репозитория
|
||||
|
||||
```bash
|
||||
HIL_VCOM_PORT=/dev/tty.usbmodemGUXFBWDJBWTGQ3 # macOS
|
||||
# HIL_VCOM_PORT=/dev/ttyACM0 # Linux
|
||||
# HIL_VCOM_PORT=COM3 # Windows
|
||||
HIL_VCOM_BAUD=115200
|
||||
HIL_READY_TIMEOUT=5.0
|
||||
HIL_PYOCD_FREQUENCY=1000000
|
||||
HIL_BUILD_DIR=build/target-debug
|
||||
```
|
||||
|
||||
Найти порт MCU-Link VCOM:
|
||||
|
||||
```bash
|
||||
just host::scan # nxpdevscan — все NXP устройства
|
||||
ls /dev/tty.usbmodem* # macOS
|
||||
ls /dev/ttyACM* # Linux
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Запуск HIL-тестов
|
||||
|
||||
Сборка target-прошивок выполняется в devcontainer:
|
||||
|
||||
```bash
|
||||
# devcontainer:
|
||||
just build::build-hil # → build/target-debug/tests/target/*/test_*.elf
|
||||
```
|
||||
|
||||
Запуск тестов — на хосте:
|
||||
|
||||
```bash
|
||||
just host::hil-run # все HIL-тесты
|
||||
just host::hil-smoke # только smoke-тесты (быстро)
|
||||
just host::hil-run-fast # без перезагрузки ELF (прошивка уже запущена)
|
||||
```
|
||||
|
||||
Или напрямую через pytest:
|
||||
|
||||
```bash
|
||||
cd tools/hil
|
||||
|
||||
# Все тесты
|
||||
uv run pytest -v
|
||||
|
||||
# Только smoke
|
||||
uv run pytest -v -m smoke
|
||||
|
||||
# Конкретный файл
|
||||
uv run pytest test_uart.py -v
|
||||
|
||||
# Без перезагрузки ELF (прошивка уже запущена)
|
||||
uv run pytest -v --no-load
|
||||
|
||||
# Другой ELF
|
||||
uv run pytest test_uart.py -v --elf /path/to/custom.elf
|
||||
|
||||
# Другой VCOM-порт
|
||||
uv run pytest -v --vcom /dev/ttyACM1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Конфигурация читается в `env_config.py`. Приоритет: CLI-опции pytest > `os.environ` > defaults.
|
||||
|
||||
| Переменная окружения | CLI pytest | Default | Описание |
|
||||
|---|---|---|---|
|
||||
| `HIL_BUILD_DIR` | — | `build/target-debug` | Директория с target ELF-файлами |
|
||||
| `HIL_VCOM_PORT` | `--vcom` | `/dev/ttyACM0` | UART-порт MCU-Link VCOM |
|
||||
| `HIL_VCOM_BAUD` | — | `115200` | Скорость UART |
|
||||
| `HIL_READY_TIMEOUT` | — | `5.0` | Таймаут ожидания `READY` от прошивки (сек) |
|
||||
| `HIL_PYOCD_FREQUENCY` | — | `1000000` | Частота SWD для загрузки ELF в RAM |
|
||||
|
||||
При запуске через `just` все переменные из корневого `.env` автоматически
|
||||
экспортируются в окружение (`set dotenv-load` + `set export`).
|
||||
|
||||
---
|
||||
|
||||
## Как работают фикстуры
|
||||
|
||||
### Цепочка зависимостей
|
||||
|
||||
```bash
|
||||
test_ping()
|
||||
└── _setup (autouse, scope=function)
|
||||
├── loaded_host_uart (scope=module) ← грузит ELF в RAM
|
||||
└── uart (scope=module) ← открывает VCOM, ждёт READY
|
||||
└── depends_on: loaded_host_uart
|
||||
```
|
||||
|
||||
`scope=module` — ELF загружается один раз на весь файл с тестами, порт
|
||||
открывается один раз. Все тесты внутри файла разделяют одно соединение.
|
||||
|
||||
### Порядок выполнения
|
||||
|
||||
```bash
|
||||
1. loaded_<n>()
|
||||
├── open_target() → pyOCD: подключиться к MCU через SWD
|
||||
├── flexram_init() → настроить ITCM/DTCM/OCRAM
|
||||
├── load_elf() → записать PT_LOAD сегменты по адресам
|
||||
└── run_from_vectors() → SP/PC из 0x00000000/0x00000004 → resume
|
||||
|
||||
2. uart()
|
||||
├── serial.Serial.open()
|
||||
├── while readline() != "READY": ... ← ждём сигнал от прошивки
|
||||
└── yield ser
|
||||
|
||||
3. test_ping(), test_echo(), ... ← тесты
|
||||
|
||||
4. uart teardown → ser.close()
|
||||
```
|
||||
|
||||
### Почему `uart` зависит от `loaded_<n>`
|
||||
|
||||
```python
|
||||
def uart(request, loaded_host_uart): # ← явная зависимость в сигнатуре
|
||||
```
|
||||
|
||||
Без этого pytest мог бы создать `uart` раньше чем ELF загружен — порт открылся
|
||||
бы, но `READY` не пришёл. Явная зависимость гарантирует порядок.
|
||||
|
||||
---
|
||||
|
||||
## pyocd_utils — справочник
|
||||
|
||||
### `open_target(frequency)`
|
||||
|
||||
Контекстный менеджер, открывает pyOCD-сессию с первым найденным пробником.
|
||||
Таргет — `cortex_m` (generic, без flash-алгоритма — для RAM-операций достаточно).
|
||||
|
||||
```python
|
||||
with open_target(frequency=1_000_000) as target:
|
||||
flexram_init(target)
|
||||
load_elf(target, "test.elf")
|
||||
run_from_vectors(target)
|
||||
```
|
||||
|
||||
### `flexram_init(target)`
|
||||
|
||||
Настраивает FLEXRAM через `IOMUXC_GPR16/GPR17`:
|
||||
|
||||
- 128 KB ITCM (0x00000000) — код
|
||||
- 128 KB DTCM (0x20000000) — данные, стек
|
||||
- 256 KB OCRAM (0x20200000) — буферы
|
||||
|
||||
### `load_elf(target, elf_path)`
|
||||
|
||||
Записывает все `PT_LOAD` сегменты ELF по физическим адресам (`p_paddr`).
|
||||
Использует `pyelftools`.
|
||||
|
||||
### `run_from_vectors(target)`
|
||||
|
||||
Читает SP и PC из таблицы векторов (ITCM `0x00000000`/`0x00000004`),
|
||||
выставляет регистры, вызывает `target.resume()`. Проверяет:
|
||||
|
||||
- SP в диапазоне DTCM `[0x20000000, 0x20040000]`
|
||||
- PC в диапазоне ITCM `[0x00000400, 0x00020000]`
|
||||
|
||||
### `load_and_run(elf_path, frequency)`
|
||||
|
||||
Комбо-функция: open_target + flexram_init + load_elf + run_from_vectors.
|
||||
Для CLI и одиночных скриптов.
|
||||
|
||||
---
|
||||
|
||||
## load_and_run.py — ручная загрузка ELF
|
||||
|
||||
Для ручной отладки без запуска тестов:
|
||||
|
||||
```bash
|
||||
# Загрузить ELF в RAM и запустить
|
||||
uv run python load_and_run.py build/target-debug/tests/target/host_uart/test_host_uart.elf
|
||||
|
||||
# Через just:
|
||||
just host::hil-load
|
||||
```
|
||||
|
||||
После этого можно подключиться к VCOM вручную:
|
||||
|
||||
```bash
|
||||
just host::uart-monitor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Протокол CLI в target-прошивках
|
||||
|
||||
Все HIL target-прошивки (`tests/target/<name>/main.c`) реализуют единый
|
||||
текстовый CLI через `bsp_uart_host`:
|
||||
|
||||
- Прошивка отправляет `READY\r\n` пока хост не открыл порт
|
||||
- Хост посылает команду строкой с `\r\n`
|
||||
- Прошивка отвечает одной строкой с `\r\n`
|
||||
|
||||
Минимальные команды в каждой прошивке:
|
||||
|
||||
| Команда | Ответ | Назначение |
|
||||
|---|---|---|
|
||||
| `PING` | `PONG` | Проверка канала |
|
||||
| `<неизвестная>` | `ERR_UNKNOWN` | Прошивка не зависает |
|
||||
|
||||
---
|
||||
|
||||
## Добавление нового HIL-теста
|
||||
|
||||
Подробный гайд — в `tests/HIL_CREATE_TEST.md`. Краткая схема:
|
||||
|
||||
```bash
|
||||
1. tests/target/<name>/main.c — C-прошивка с CLI
|
||||
2. tests/target/<name>/CMakeLists.txt — сборка с bsp_boot_ram
|
||||
3. tests/target/CMakeLists.txt — add_subdirectory(<name>)
|
||||
4. CMakePresets.json — добавить test_<name> в target-debug-build
|
||||
5. tools/hil/test_<name>.py — pytest-тесты
|
||||
6. tools/hil/conftest.py — добавить loaded_<name> фикстуру
|
||||
```
|
||||
|
||||
Фикстура для нового теста в `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",
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Совместимость с GDB-сервером
|
||||
|
||||
pyOCD используется для двух независимых задач:
|
||||
|
||||
| Задача | Команда | Таргет | Порт |
|
||||
|---|---|---|---|
|
||||
| HIL-тесты (RAM) | `pyocd` через `pyocd_utils` | `cortex_m` | — |
|
||||
| GDB-сервер (отладка) | `pyocd gdbserver` | `mimxrt1050_quadspi` | 3333 |
|
||||
|
||||
**MCU-Link монопольный** — нельзя запускать HIL и GDB-сервер одновременно.
|
||||
Перед `just host::hil-run` остановите GDB-сервер (`Ctrl+C`), и наоборот.
|
||||
|
||||
Разные таргеты намеренны: HIL не нужен flash-алгоритм (`cortex_m` достаточно),
|
||||
GDB-сервер нужен полноценный таргет для корректного reset и SVD.
|
||||
|
|
|
|||
|
|
@ -89,52 +89,10 @@ def _open_uart_and_wait_ready(request: pytest.FixtureRequest) -> serial.Serial:
|
|||
return ser
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Фикстуры загрузки (scope=module — один раз на файл с тестами)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def loaded_host_uart(request: pytest.FixtureRequest) -> None:
|
||||
_load_elf(
|
||||
request,
|
||||
Path(cfg.BUILD_DIR) / "tests/target/host_uart/test_host_uart.elf",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def loaded_hil_opto(request: pytest.FixtureRequest, m5: M5Agent) -> None:
|
||||
"""Загрузить test_hil_opto.elf. Зависит от m5 — таргет должен быть запитан."""
|
||||
_load_elf(
|
||||
request,
|
||||
Path(cfg.BUILD_DIR) / "tests/target/hil_opto/test_hil_opto.elf",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Фикстура UART — открывает порт и ждёт "READY\r\n" от прошивки
|
||||
# ---------------------------------------------------------------------------
|
||||
@pytest.fixture(scope="module")
|
||||
def uart(
|
||||
request: pytest.FixtureRequest,
|
||||
loaded_host_uart,
|
||||
) -> Generator[serial.Serial, None, None]:
|
||||
ser = _open_uart_and_wait_ready(request)
|
||||
yield ser
|
||||
ser.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def uart_opto(
|
||||
request: pytest.FixtureRequest,
|
||||
loaded_hil_opto,
|
||||
) -> Generator[serial.Serial, None, None]:
|
||||
ser = _open_uart_and_wait_ready(request)
|
||||
yield ser
|
||||
ser.close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# M5StampPLC — драйвер для pytest (JSON-lines протокол)
|
||||
#
|
||||
# Определяем РАНЬШЕ фикстур, которые его используют в аннотациях.
|
||||
# ---------------------------------------------------------------------------
|
||||
class M5Agent:
|
||||
"""Драйвер M5StampPLC для pytest (JSON-lines протокол через USB CDC)."""
|
||||
|
|
@ -240,6 +198,58 @@ def m5(request: pytest.FixtureRequest) -> Generator[M5Agent, None, None]:
|
|||
ser.close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Фикстуры загрузки (scope=module — один раз на файл с тестами)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def loaded_host_uart(request: pytest.FixtureRequest) -> None:
|
||||
_load_elf(
|
||||
request,
|
||||
Path(cfg.BUILD_DIR) / "tests/target/host_uart/test_host_uart.elf",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def loaded_hil_opto(request: pytest.FixtureRequest, m5: M5Agent) -> None:
|
||||
"""
|
||||
Загрузить test_hil_opto.elf.
|
||||
|
||||
Явная зависимость от фикстуры m5 гарантирует порядок:
|
||||
1. m5 создаётся первым → питание таргета включено
|
||||
2. только потом pyOCD подключается и грузит ELF
|
||||
Без этой зависимости pytest мог бы попытаться подключиться
|
||||
к MCU пока он ещё обесточен.
|
||||
"""
|
||||
_load_elf(
|
||||
request,
|
||||
Path(cfg.BUILD_DIR) / "tests/target/hil_opto/test_hil_opto.elf",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Фикстуры UART
|
||||
# ---------------------------------------------------------------------------
|
||||
@pytest.fixture(scope="module")
|
||||
def uart(
|
||||
request: pytest.FixtureRequest,
|
||||
loaded_host_uart,
|
||||
) -> Generator[serial.Serial, None, None]:
|
||||
ser = _open_uart_and_wait_ready(request)
|
||||
yield ser
|
||||
ser.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def uart_opto(
|
||||
request: pytest.FixtureRequest,
|
||||
loaded_hil_opto,
|
||||
) -> Generator[serial.Serial, None, None]:
|
||||
ser = _open_uart_and_wait_ready(request)
|
||||
yield ser
|
||||
ser.close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Утилита для тестов
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ m5/agent.py — MicroPython агент для M5Stack StamPLC.
|
|||
|
||||
Раскладка реле (конфигурация стенда):
|
||||
RLY1 → питание таргета (VIN)
|
||||
RLY2 → EXT_IN1 таргета (BSP_OPTO_CH_IN1)
|
||||
RLY3 → EXT_IN2 таргета (BSP_OPTO_CH_IN2)
|
||||
RLY4 → RS_RX таргета (BSP_OPTO_CH_RS)
|
||||
RLY2 → RS_RX таргета (BSP_OPTO_CH_RS)
|
||||
RLY3 → EXT_IN1 таргета (BSP_OPTO_CH_IN1)
|
||||
RLY4 → EXT_IN2 таргета (BSP_OPTO_CH_IN2)
|
||||
|
||||
─────────────────────────────────────────────────────
|
||||
Протокол: JSON-lines через USB CDC (115200, нет flow control).
|
||||
|
|
@ -69,7 +69,10 @@ _CFG = {
|
|||
|
||||
_RST_PIN = 3 # G3_PHY_RST → нога сброса AW9523B
|
||||
|
||||
_OPTO_TO_RELAY = {1: 2, 2: 3, 3: 4} # оптоканал таргета → реле стенда
|
||||
_OPTO_TO_RELAY = {1: 3, 2: 4, 3: 2} # оптоканал таргета → реле стенда
|
||||
# ch1 (BSP_OPTO_CH_IN1 / EXT_IN1) → RLY3
|
||||
# ch2 (BSP_OPTO_CH_IN2 / EXT_IN2) → RLY4
|
||||
# ch3 (BSP_OPTO_CH_RS / RS_RX ) → RLY2
|
||||
_IN_PIN_LIST = [4, 5, 6, 7, 12, 13, 14, 15] # нумерация пинов AW9523
|
||||
# ---------------------------------------------------------------------------
|
||||
# AW9523B — регистры
|
||||
|
|
|
|||
|
|
@ -97,6 +97,14 @@ def _send(ser: serial.Serial, cmd: dict) -> dict:
|
|||
|
||||
|
||||
def _wait_ready(ser: serial.Serial) -> None:
|
||||
"""
|
||||
Ждём READY от агента.
|
||||
|
||||
Два сценария:
|
||||
1. Агент только что запустился (после reset/deploy) → пришлёт READY.
|
||||
2. Агент уже работает → READY давно отправлен, не придёт снова.
|
||||
Fallback: пробуем ping. Если отвечает — считаем что всё ок.
|
||||
"""
|
||||
print(_info(" Ожидаем READY от агента..."), end=" ", flush=True)
|
||||
deadline = time.monotonic() + _READY_WAIT
|
||||
while time.monotonic() < deadline:
|
||||
|
|
@ -104,8 +112,20 @@ def _wait_ready(ser: serial.Serial) -> None:
|
|||
if line == "READY":
|
||||
print(_ok("OK"))
|
||||
return
|
||||
print(_err("TIMEOUT"))
|
||||
print(_err(" Агент не ответил. Проверьте порт и что m5/agent.py загружен (just host::m5-deploy)."))
|
||||
|
||||
# READY не пришёл — агент, вероятно, уже работает. Пробуем ping.
|
||||
print(_hint("(не получен, пробуем ping...)"), end=" ", flush=True)
|
||||
try:
|
||||
resp = _send(ser, {"cmd": "ping"})
|
||||
if resp.get("ok"):
|
||||
print(_ok("OK (агент уже работал)"))
|
||||
return
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
print(_err("FAILED"))
|
||||
print(_err(" Агент не отвечает. Проверьте порт и задеплойте агент:"))
|
||||
print(_hint(" just host::m5-deploy"))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# Загрузчики с microPython
|
||||
|
||||
> На HIL-стенд m5stamPLC необходимо предварительно загрузить интерпретор `microPython`.
|
||||
|
|
@ -2,16 +2,17 @@
|
|||
test_opto.py — HIL тест bsp_opto через M5StampPLC.
|
||||
|
||||
Стенд:
|
||||
M5StampPLC реле → оптопары таргета (PS2801-4, active-LOW):
|
||||
M5StampPLC реле → оптопары таргета (PS2801-4, active-HIGH, неинвертирующие):
|
||||
RLY2 → EXT_IN1 (ch1, BSP_OPTO_CH_IN1)
|
||||
RLY3 → EXT_IN2 (ch2, BSP_OPTO_CH_IN2)
|
||||
RLY4 → RS_RX (ch3, BSP_OPTO_CH_RS)
|
||||
|
||||
Схема фикстур:
|
||||
conftest.loaded_hil_opto → pyocd_utils: FLEXRAM + load ELF + run
|
||||
conftest.uart_opto → pyserial: открыть VCOM, ждать READY
|
||||
conftest.m5 → M5Agent: JSON-lines к M5StampPLC
|
||||
тесты → uart_cmd() + m5.opto_set() → assert
|
||||
Цепочка фикстур (scope=module, создаются один раз на весь файл):
|
||||
|
||||
m5 (питание ON + opto_all_off)
|
||||
└── loaded_hil_opto (грузит ELF через pyOCD)
|
||||
└── uart_opto (открывает VCOM, ждёт READY)
|
||||
└── _setup (autouse, function scope) → self.ser / self.m5
|
||||
|
||||
Запуск:
|
||||
just host::hil-opto
|
||||
|
|
@ -24,16 +25,70 @@ import time
|
|||
import pytest
|
||||
from conftest import uart_cmd
|
||||
|
||||
# Задержка после переключения реле перед чтением состояния (с).
|
||||
# Включает: механическое переключение реле (~5ms), время отклика
|
||||
# оптопары (~50µs), ISR + process() цикл, UART round-trip (~2ms).
|
||||
RELAY_SETTLE_S = 0.05
|
||||
# ---------------------------------------------------------------------------
|
||||
# Временны́е константы
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# После переключения реле ждём:
|
||||
# реле механика ~10 мс
|
||||
# оптопара ~0.1 мс
|
||||
# debounce 10 мс (debounce_ms в прошивке)
|
||||
# process() цикл ~10 мс (CLI_RX_TIMEOUT)
|
||||
# UART round-trip ~2 мс
|
||||
# Итого минимум ~33 мс. Берём с запасом × 3.
|
||||
RELAY_ON_S = 0.150 # ждать после включения реле
|
||||
|
||||
# После выключения нужно убедиться что INACTIVE подтверждён дебаунсом.
|
||||
# Берём чуть больше чем RELAY_ON_S — дебаунс работает симметрично.
|
||||
RELAY_OFF_S = 0.150
|
||||
|
||||
|
||||
# ── Связь ──────────────────────────────────────────────────────────────────
|
||||
# ---------------------------------------------------------------------------
|
||||
# Вспомогательные функции
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def opto_read(ser, ch: int) -> str:
|
||||
"""Прочитать состояние канала (1-based). Возвращает 'ACTIVE' или 'INACTIVE'."""
|
||||
return uart_cmd(ser, f"OPTO_READ {ch}")
|
||||
|
||||
|
||||
def reset_events(ser) -> None:
|
||||
"""Сбросить счётчик событий на таргете."""
|
||||
assert uart_cmd(ser, "OPTO_RESET_EVENTS") == "OK"
|
||||
|
||||
|
||||
def all_off_and_confirm(m5, ser, timeout_s: float = 1.0) -> None:
|
||||
"""
|
||||
Выключить все реле и дождаться пока ВСЕ каналы подтвердят INACTIVE.
|
||||
|
||||
Простого time.sleep() недостаточно: дебаунс + цикл process() могут
|
||||
занимать переменное время. Активно опрашиваем таргет пока не убедимся
|
||||
что состояние стабильно.
|
||||
"""
|
||||
m5.opto_all_off()
|
||||
|
||||
deadline = time.monotonic() + timeout_s
|
||||
while time.monotonic() < deadline:
|
||||
time.sleep(0.02)
|
||||
states = [opto_read(ser, ch) for ch in [1, 2, 3]]
|
||||
if all(s == "INACTIVE" for s in states):
|
||||
return
|
||||
|
||||
# Если вышли по таймауту — сообщаем что именно осталось активным
|
||||
states = [opto_read(ser, ch) for ch in [1, 2, 3]]
|
||||
active = [i + 1 for i, s in enumerate(states) if s == "ACTIVE"]
|
||||
raise TimeoutError(
|
||||
f"Каналы {active} остались ACTIVE после opto_all_off() "
|
||||
f"и ожидания {timeout_s} с. Проверьте схему стенда."
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Проверка каналов связи
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestOptoConnectivity:
|
||||
"""Проверка каналов связи с таргетом и M5."""
|
||||
"""Базовая проверка: таргет и M5 отвечают."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self, uart_opto, m5):
|
||||
|
|
@ -49,7 +104,9 @@ class TestOptoConnectivity:
|
|||
self.m5.ping()
|
||||
|
||||
|
||||
# ── Состояние по умолчанию ─────────────────────────────────────────────────
|
||||
# ---------------------------------------------------------------------------
|
||||
# Состояние по умолчанию (все реле выключены)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestOptoReadDefault:
|
||||
"""Все каналы INACTIVE без внешнего воздействия."""
|
||||
|
|
@ -58,21 +115,22 @@ class TestOptoReadDefault:
|
|||
def _setup(self, uart_opto, m5):
|
||||
self.ser = uart_opto
|
||||
self.m5 = m5
|
||||
self.m5.opto_all_off()
|
||||
time.sleep(RELAY_SETTLE_S)
|
||||
all_off_and_confirm(self.m5, self.ser)
|
||||
|
||||
def test_ch1_default_inactive(self):
|
||||
assert uart_cmd(self.ser, "OPTO_READ 1") == "INACTIVE"
|
||||
assert opto_read(self.ser, 1) == "INACTIVE"
|
||||
|
||||
def test_ch2_default_inactive(self):
|
||||
assert uart_cmd(self.ser, "OPTO_READ 2") == "INACTIVE"
|
||||
assert opto_read(self.ser, 2) == "INACTIVE"
|
||||
|
||||
def test_ch3_default_inactive(self):
|
||||
"""RS_RX (ch3) — сконфигурирован как GPIO, должен быть INACTIVE."""
|
||||
assert uart_cmd(self.ser, "OPTO_READ 3") == "INACTIVE"
|
||||
"""RS_RX (ch3) — сконфигурирован как GPIO (MODE_LEVEL), должен быть INACTIVE."""
|
||||
assert opto_read(self.ser, 3) == "INACTIVE"
|
||||
|
||||
|
||||
# ── Активация / деактивация каналов ────────────────────────────────────────
|
||||
# ---------------------------------------------------------------------------
|
||||
# Активация / деактивация каналов
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestOptoActivateDeactivate:
|
||||
"""Активация/деактивация каждого канала по отдельности через M5."""
|
||||
|
|
@ -81,33 +139,98 @@ class TestOptoActivateDeactivate:
|
|||
def _setup(self, uart_opto, m5):
|
||||
self.ser = uart_opto
|
||||
self.m5 = m5
|
||||
self.m5.opto_all_off()
|
||||
time.sleep(RELAY_SETTLE_S)
|
||||
all_off_and_confirm(self.m5, self.ser)
|
||||
|
||||
@pytest.mark.parametrize("ch", [1, 2, 3])
|
||||
def test_activate_single_channel(self, ch):
|
||||
"""M5 opto ON → таргет читает ACTIVE."""
|
||||
self.m5.opto_set(ch, True)
|
||||
time.sleep(RELAY_SETTLE_S)
|
||||
assert uart_cmd(self.ser, f"OPTO_READ {ch}") == "ACTIVE"
|
||||
time.sleep(RELAY_ON_S)
|
||||
assert opto_read(self.ser, ch) == "ACTIVE"
|
||||
|
||||
@pytest.mark.parametrize("ch", [1, 2, 3])
|
||||
def test_deactivate_single_channel(self, ch):
|
||||
"""ON → OFF → таргет читает INACTIVE."""
|
||||
self.m5.opto_set(ch, True)
|
||||
time.sleep(RELAY_SETTLE_S)
|
||||
time.sleep(RELAY_ON_S)
|
||||
self.m5.opto_set(ch, False)
|
||||
time.sleep(RELAY_SETTLE_S)
|
||||
assert uart_cmd(self.ser, f"OPTO_READ {ch}") == "INACTIVE"
|
||||
time.sleep(RELAY_OFF_S)
|
||||
assert opto_read(self.ser, ch) == "INACTIVE"
|
||||
|
||||
@pytest.mark.parametrize("ch", [1, 2, 3])
|
||||
def test_isolation(self, ch):
|
||||
"""Активация одного канала не влияет на остальные."""
|
||||
others = [c for c in [1, 2, 3] if c != ch]
|
||||
self.m5.opto_set(ch, True)
|
||||
time.sleep(RELAY_SETTLE_S)
|
||||
time.sleep(RELAY_ON_S)
|
||||
for other in others:
|
||||
assert uart_cmd(self.ser, f"OPTO_READ {other}") == "INACTIVE", \
|
||||
assert opto_read(self.ser, other) == "INACTIVE", \
|
||||
f"ch{other} должен быть INACTIVE когда активен только ch{ch}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Callback-события (проверяет ISR + debounce + bsp_opto_process)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestOptoEvents:
|
||||
"""
|
||||
Проверяет что прерывание + bsp_opto_process() + коллбэк корректно
|
||||
регистрируют события.
|
||||
|
||||
Таргет считает события через s_event_count / s_last_ch / s_last_state,
|
||||
доступные через команды OPTO_EVENTS / OPTO_LAST_EVENT / OPTO_RESET_EVENTS.
|
||||
"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self, uart_opto, m5):
|
||||
self.ser = uart_opto
|
||||
self.m5 = m5
|
||||
all_off_and_confirm(self.m5, self.ser)
|
||||
reset_events(self.ser)
|
||||
|
||||
def test_activate_generates_event(self):
|
||||
"""Включение реле генерирует минимум одно событие ACTIVE."""
|
||||
self.m5.opto_set(1, True)
|
||||
time.sleep(RELAY_ON_S)
|
||||
|
||||
count = int(uart_cmd(self.ser, "OPTO_EVENTS"))
|
||||
assert count >= 1, f"Ожидали >= 1 событие, получили {count}"
|
||||
|
||||
last = uart_cmd(self.ser, "OPTO_LAST_EVENT")
|
||||
ch_str, state_str = last.split()
|
||||
assert ch_str == "1"
|
||||
assert state_str == "ACTIVE"
|
||||
|
||||
def test_deactivate_generates_event(self):
|
||||
"""Выключение реле генерирует событие INACTIVE."""
|
||||
self.m5.opto_set(1, True)
|
||||
time.sleep(RELAY_ON_S)
|
||||
reset_events(self.ser)
|
||||
|
||||
self.m5.opto_set(1, False)
|
||||
time.sleep(RELAY_OFF_S)
|
||||
|
||||
count = int(uart_cmd(self.ser, "OPTO_EVENTS"))
|
||||
assert count >= 1, f"Ожидали >= 1 событие после выключения, получили {count}"
|
||||
|
||||
last = uart_cmd(self.ser, "OPTO_LAST_EVENT")
|
||||
ch_str, state_str = last.split()
|
||||
assert ch_str == "1"
|
||||
assert state_str == "INACTIVE"
|
||||
|
||||
def test_reset_events_clears_counter(self):
|
||||
"""OPTO_RESET_EVENTS обнуляет счётчик."""
|
||||
self.m5.opto_set(1, True)
|
||||
time.sleep(RELAY_ON_S)
|
||||
|
||||
reset_events(self.ser)
|
||||
assert uart_cmd(self.ser, "OPTO_EVENTS") == "0"
|
||||
|
||||
def test_correct_channel_reported_in_last_event(self):
|
||||
"""OPTO_LAST_EVENT сообщает правильный канал."""
|
||||
self.m5.opto_set(2, True)
|
||||
time.sleep(RELAY_ON_S)
|
||||
|
||||
last = uart_cmd(self.ser, "OPTO_LAST_EVENT")
|
||||
ch_str, _ = last.split()
|
||||
assert ch_str == "2", f"Ожидали ch=2, получили {ch_str!r}"
|
||||
|
|
@ -1,329 +1,58 @@
|
|||
# tools/host — Окружение прошивки MIMXRT1052CVJ5B
|
||||
# tools/host
|
||||
|
||||
Изолированное Python-окружение на базе [uv](https://docs.astral.sh/uv/) для сборки
|
||||
HAB-образов и прошивки платы. Запускается на хост-машине разработчика —
|
||||
**не внутри devcontainer**.
|
||||
Python-окружение на базе [uv](https://docs.astral.sh/uv/) для сборки HAB-образов
|
||||
и прошивки платы через USB SDP или SWD.
|
||||
|
||||
Поддерживаются два независимых способа прошивки:
|
||||
|
||||
| Способ | Скрипт | Интерфейс | Требование |
|
||||
|---|---|---|---|
|
||||
| USB SDP | `flash_usb.py` | USB ↔ ROM-загрузчик | BOOT_MODE = 01 |
|
||||
| SWD | `flash_swd.py` | MCU-Link ↔ CMSIS-DAP | Плата в любом режиме |
|
||||
Запускается на **хост-машине** — не внутри devcontainer.
|
||||
|
||||
---
|
||||
|
||||
## Структура
|
||||
|
||||
```bash
|
||||
```иbash
|
||||
tools/host/
|
||||
├── flash_usb.py — прошивка через USB ROM: sdphost → Flashloader → Flash
|
||||
├── flash_swd.py — прошивка через SWD: FCB + HAB → pyOCD → Flash
|
||||
├── hab/ — HAB yaml-конфиги для nxpimage (по одному на проект × тип)
|
||||
├── dcd/
|
||||
│ ├── dcd.bin ← DCD бинарник (инициализация SDRAM)
|
||||
│ ├── ivt_flashloader.bin ← NXP Flashloader (USB SDP)
|
||||
│ ├── w25q64_fdcb.bin ← FCB для W25Q64 (SWD flash)
|
||||
│ ├── w25q128_fdcb.bin ← FCB для W25Q128 (SWD flash) ← используется
|
||||
│ └── w25q512_fdcb.bin ← FCB для W25Q512 (SWD flash)
|
||||
├── hab/
|
||||
│ ├── hab_firmware_test_debug.yaml
|
||||
│ ├── hab_firmware_test_release.yaml
|
||||
│ ├── hab_bootloader_debug.yaml
|
||||
│ ├── hab_bootloader_release.yaml
|
||||
│ ├── hab_app_debug.yaml
|
||||
│ └── hab_app_release.yaml
|
||||
├── flash_usb.py ← прошивка через USB ROM (SDP → blhost)
|
||||
├── flash_swd.py ← прошивка через SWD (pyocd, FCB+HAB)
|
||||
├── HAB_GUIDE.md
|
||||
│ ├── ivt_flashloader.bin — NXP Flashloader (загружается в RAM через SDP)
|
||||
│ ├── dcd.bin — DCD: инициализация SDRAM (SEMC + MT48LC16M16A2P)
|
||||
│ ├── w25q128_fdcb.bin — FCB для W25Q128 Quad SPI ← используется
|
||||
│ ├── w25q64_fdcb.bin — FCB для W25Q64 Quad SPI
|
||||
│ └── w25q512_fdcb.bin — FCB для W25Q512 Quad SPI
|
||||
├── HAB_GUIDE.md — подробно про HAB-образы и процесс подписи
|
||||
├── pyproject.toml
|
||||
├── uv.lock
|
||||
└── README.md
|
||||
└── uv.lock
|
||||
```
|
||||
|
||||
> Все бинарники в `dcd/` получены из NXP SecureProvisioningTool и хранятся
|
||||
> в репозитории — пересоздавать не нужно.
|
||||
|
||||
---
|
||||
|
||||
## Предварительные требования
|
||||
## Документация
|
||||
|
||||
### 1. uv — один раз на машину
|
||||
|
||||
```bash
|
||||
# macOS / Linux
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# Windows
|
||||
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
|
||||
```
|
||||
|
||||
### 2. Зависимости проекта — один раз на репозиторий
|
||||
|
||||
```bash
|
||||
cd tools/host
|
||||
uv sync
|
||||
```
|
||||
|
||||
### 3. udev правила — только Linux, один раз на машину
|
||||
|
||||
```bash
|
||||
sudo tee /etc/udev/rules.d/99-nxp-mimxrt.rules << 'EOF'
|
||||
# NXP BootROM — SDP режим
|
||||
SUBSYSTEM=="usb", ATTR{idVendor}=="1fc9", ATTR{idProduct}=="0130", MODE="0666", GROUP="plugdev"
|
||||
# NXP Flashloader
|
||||
SUBSYSTEM=="usb", ATTR{idVendor}=="15a2", ATTR{idProduct}=="0073", MODE="0666", GROUP="plugdev"
|
||||
EOF
|
||||
|
||||
sudo udevadm control --reload-rules && sudo udevadm trigger
|
||||
sudo usermod -a -G plugdev $USER
|
||||
# После usermod — перелогиниться!
|
||||
```
|
||||
Подробное описание обоих способов прошивки — в `docs/HOW_TO_FLASH.md`.
|
||||
Сравнительная таблица, карта Flash, диагностика — там же.
|
||||
|
||||
---
|
||||
|
||||
## Карта Flash
|
||||
## Быстрый старт
|
||||
|
||||
```bash
|
||||
0x60000000 ┌─────────────────────────────┐
|
||||
│ FCB — Flash Config Block │ 512 байт
|
||||
│ При USB SDP: пишет │
|
||||
│ Flashloader автоматически. │
|
||||
│ При SWD: flash_swd.py │
|
||||
│ берёт из dcd/*_fdcb.bin. │
|
||||
0x60001000 ├─────────────────────────────┤
|
||||
│ IVT — Image Vector Table │ ← начало HAB-образа
|
||||
│ BDT — Boot Data Table │
|
||||
0x60001040 ├─────────────────────────────┤
|
||||
│ DCD — SDRAM init │ ~1088 байт (fw_test, app)
|
||||
0x60003000 ├─────────────────────────────┤
|
||||
│ Код прошивки │
|
||||
│ (.text, .data, ...) │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
# Первый раз: установить зависимости
|
||||
cd tools/host && uv sync
|
||||
|
||||
---
|
||||
# HAB-образы собираются внутри devcontainer:
|
||||
just build::hab-firmware-test-debug
|
||||
just build::hab-all-release
|
||||
|
||||
## Способ 1 — USB SDP (`flash_usb.py`)
|
||||
# Прошивка через USB SDP (BOOT_MOD_1 → 3V3 → Reset):
|
||||
just host::flash-test-debug
|
||||
just host::flash-test-release
|
||||
just host::flash-production # bootloader + app release
|
||||
|
||||
Прошивка через ROM-загрузчик. Требует перевода платы в режим Serial Downloader.
|
||||
|
||||
### Подготовка платы
|
||||
|
||||
```bash
|
||||
1. BOOT_MOD_1 → 3V3
|
||||
2. Reset
|
||||
3. Подключить USB → плата определяется как VID:PID 1FC9:0130
|
||||
```
|
||||
|
||||
После прошивки: `BOOT_MOD_1 → GND → Reset`.
|
||||
|
||||
### Запуск
|
||||
|
||||
```bash
|
||||
# Предпочтительно через just (на хосте):
|
||||
just host::flash firmware_test debug
|
||||
just host::flash firmware_test release
|
||||
just host::flash bootloader release
|
||||
just host::flash app release
|
||||
|
||||
# Или напрямую:
|
||||
cd tools/host
|
||||
uv run python3 flash_usb.py --firmware firmware_test --build-type Debug
|
||||
uv run python3 flash_usb.py --firmware app --build-type Release
|
||||
|
||||
# Загрузка в RAM без записи во Flash (быстро, не изнашивает Flash):
|
||||
uv run python3 flash_usb.py --firmware firmware_test --build-type Debug --ram-only
|
||||
```
|
||||
|
||||
### Последовательность команд
|
||||
|
||||
```bash
|
||||
Плата в SDP режиме (1FC9:0130)
|
||||
│
|
||||
├─ sdphost write-file 0x20001C00 ivt_flashloader.bin
|
||||
└─ sdphost jump-address 0x20001C00
|
||||
│
|
||||
│ (ожидание до 10с пока Flashloader поднимется на 15A2:0073)
|
||||
│
|
||||
Flashloader (15A2:0073)
|
||||
├─ configure-memory 0xC0000007 — инициализация FlexSPI NOR
|
||||
├─ flash-erase-region 0x60000000
|
||||
├─ configure-memory 0xF000000F — запись FCB в 0x60000000
|
||||
├─ write-memory 0x60001000 ← HAB-образ
|
||||
└─ reset
|
||||
```
|
||||
|
||||
FCB генерируется Flashloader'ом автоматически из параметров FlexSPI — отдельный
|
||||
`*_fdcb.bin` не нужен.
|
||||
|
||||
---
|
||||
|
||||
## Способ 2 — SWD (`flash_swd.py`)
|
||||
|
||||
Прошивка через отладочный пробник (MCU-Link, CMSIS-DAP). Плата остаётся в
|
||||
нормальном режиме загрузки — переключать `BOOT_MOD_1` не нужно.
|
||||
|
||||
**После записи обязателен power cycle** — VECTRESET не реинициализирует FlexSPI,
|
||||
Boot ROM не стартует без холодного старта.
|
||||
|
||||
### Почему нужен FCB при SWD
|
||||
|
||||
При USB SDP FlexSPI конфигурируется ROM-загрузчиком через DCD. При SWD
|
||||
flash-алгоритм pyOCD пишет данные напрямую — Boot ROM при cold-start читает FCB
|
||||
первым и по нему конфигурирует FlexSPI. Без FCB плата не стартует.
|
||||
|
||||
`flash_swd.py` собирает итоговый образ перед записью:
|
||||
|
||||
```
|
||||
0x60000000 *_fdcb.bin (512 байт) — FCB
|
||||
0x60000200 0xFF × 3584 байт — padding (erased flash value)
|
||||
0x60001000 *_hab.bin — HAB-образ (ivtOffset = 0x1000)
|
||||
```
|
||||
|
||||
Всё умещается в один 64KB-сектор — стирается и записывается за одну транзакцию.
|
||||
|
||||
### FCB-файлы
|
||||
|
||||
| Файл | Микросхема | Режим |
|
||||
|---|---|---|
|
||||
| `w25q64_fdcb.bin` | Winbond W25Q64 | Quad SPI |
|
||||
| `w25q128_fdcb.bin` | Winbond W25Q128 | Quad SPI |
|
||||
| `w25q512_fdcb.bin` | Winbond W25Q512 | Quad SPI |
|
||||
|
||||
Активный FCB задаётся в `.env`: `FCB_PATH=tools/host/dcd/w25q128_fdcb.bin`.
|
||||
|
||||
FCB-файлы получены из NXP SecureProvisioningTool и хранятся в репозитории —
|
||||
пересоздавать не нужно.
|
||||
|
||||
### Запуск
|
||||
|
||||
```bash
|
||||
# Предпочтительно через just (на хосте):
|
||||
# Прошивка через SWD (power cycle обязателен после):
|
||||
just host::flash-swd-test-debug
|
||||
just host::flash-swd-test-release
|
||||
just host::flash-swd-bootloader-debug
|
||||
just host::flash-swd-bootloader-release
|
||||
just host::flash-swd-app-debug
|
||||
just host::flash-swd-app-release
|
||||
|
||||
# Или напрямую:
|
||||
cd tools/host
|
||||
uv run --directory ../hil python3 flash_swd.py --firmware firmware_test --build-type Debug
|
||||
uv run --directory ../hil python3 flash_swd.py --firmware app --build-type Release
|
||||
|
||||
# Собрать образ без записи (для проверки):
|
||||
uv run --directory ../hil python3 flash_swd.py --firmware firmware_test --build-type Debug --dry-run
|
||||
```
|
||||
|
||||
### Конфигурация flash_swd.py
|
||||
|
||||
Приоритет: аргументы CLI > переменные окружения > defaults.
|
||||
|
||||
| Переменная | CLI-аргумент | Default |
|
||||
|---|---|---|
|
||||
| `PYOCD_TARGET` | `--target` | `mimxrt1050_quadspi` |
|
||||
| `PYOCD_FREQUENCY` | `--frequency` | `4000000` |
|
||||
| `BUILD_DIR` | — | `<repo>/build` |
|
||||
| `FCB_PATH` | `--fcb` | `tools/host/dcd/w25q128_fdcb.bin` |
|
||||
|
||||
Переменные задаются в `.env` и экспортируются через `just` (`set export`).
|
||||
|
||||
---
|
||||
|
||||
## Сравнение способов
|
||||
|
||||
| | USB SDP | SWD |
|
||||
|---|---|---|
|
||||
| Переключение BOOT_MODE | Нужно | Не нужно |
|
||||
| Power cycle после записи | Не нужен | **Обязателен** |
|
||||
| FCB в образе | Не нужен (Flashloader пишет сам) | **Нужен** (`*_fdcb.bin`) |
|
||||
| Скорость записи | ~50–100 kB/s | ~8–10 kB/s |
|
||||
| Совместимость с отладкой | Раздельно | MCU-Link монопольный |
|
||||
| Производственный сценарий | ✓ | — |
|
||||
| Итеративная разработка | Неудобно (смена режима) | ✓ |
|
||||
|
||||
---
|
||||
|
||||
## Пайплайн: сборка HAB-образов
|
||||
|
||||
Выполняется **внутри devcontainer**:
|
||||
|
||||
```bash
|
||||
just build::hab-firmware-test-debug # → build/Debug/firmware_test_hab.bin
|
||||
just build::hab-firmware-test-release # → build/Release/firmware_test_hab.bin
|
||||
just build::hab-bootloader-debug
|
||||
just build::hab-bootloader-release
|
||||
just build::hab-app-debug
|
||||
just build::hab-app-release
|
||||
just build::hab-all-release # все три Release за один раз
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DCD — инициализация SDRAM
|
||||
|
||||
`dcd/dcd.bin` — бинарный блоб команд, который BootROM выполняет до передачи
|
||||
управления прошивке. Инициализирует PLL, CCM clock gates, SEMC контроллер
|
||||
и микросхему SDRAM (MT48LCxxM4).
|
||||
|
||||
Файл хранится в репозитории в бинарном виде и **не требует пересборки**.
|
||||
|
||||
```bash
|
||||
Цель: MT48LC16M16A2P-6A, 32 MB, шина 16 бит, CS0
|
||||
SEMC BR0: base=0x80000000, size=32MB, VLD=1
|
||||
SEMC BR1–BR3: VLD=0
|
||||
```
|
||||
|
||||
| Прошивка | DCD | Причина |
|
||||
|---|---|---|
|
||||
| `firmware_test` | ✓ | тесты работают с SDRAM |
|
||||
| `app` | ✓ | FreeRTOS heap и буферы LCDIF в SDRAM |
|
||||
| `bootloader` | ✗ | загрузчик не использует SDRAM |
|
||||
|
||||
---
|
||||
|
||||
## Flashloader
|
||||
|
||||
`dcd/ivt_flashloader.bin` — NXP-программа, загружаемая в RAM через SDP.
|
||||
|
||||
```bash
|
||||
Entry point: 0x20002401
|
||||
Загрузка по: 0x20001C00
|
||||
VID:PID после: 15A2:0073
|
||||
Источник: MCUXpresso Secure Provisioning Tool 25.12
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Диагностика
|
||||
|
||||
```bash
|
||||
# Найти подключённые NXP устройства
|
||||
uv run nxpdevscan
|
||||
|
||||
# Проверить связь с BootROM (плата в SDP-режиме)
|
||||
sdphost -u 0x1FC9,0x0130 -- error-status
|
||||
|
||||
# Проверить что Flashloader отвечает
|
||||
blhost -u 0x15A2,0x0073 -- get-property 1 0
|
||||
|
||||
# Проверить что pyOCD видит таргет (для SWD)
|
||||
just host::debug-list-targets
|
||||
```
|
||||
|
||||
### Типичные ошибки
|
||||
|
||||
| Симптом | Причина | Решение |
|
||||
|---|---|---|
|
||||
| `USB HID device not found: 1FC9:0130` | Плата не в SDP режиме | Проверить `BOOT_MOD_1` → 3V3 и Reset |
|
||||
| Flashloader timeout после jump | `ivt_flashloader.bin` повреждён | Взять из SPT 25.12 |
|
||||
| Плата не стартует после USB SDP | `BOOT_MOD_1` не переключён обратно | `BOOT_MOD_1` → GND, Reset |
|
||||
| Плата не стартует после SWD flash | Power cycle не был выполнен | Отключить и подключить питание |
|
||||
| Плата не стартует после SWD flash | Неверный FCB (другая Flash-микросхема) | Проверить `FCB_PATH` в `.env` |
|
||||
| `skipped N bytes` при SWD flash | pyOCD считает содержимое актуальным | Добавить `--erase chip` или `--erase sector` |
|
||||
|
||||
---
|
||||
|
||||
## Обновление зависимостей
|
||||
|
||||
```bash
|
||||
cd tools/host
|
||||
uv add "spsdk==X.Y.Z"
|
||||
uv sync
|
||||
git add uv.lock pyproject.toml
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# utils
|
||||
|
||||
Платформо-независимые утилиты проекта.
|
||||
Платформонезависимые утилиты проекта.
|
||||
|
||||
**Правило включения** — код попадает сюда только если выполняются оба условия:
|
||||
|
||||
|
|
@ -11,5 +11,9 @@
|
|||
|
||||
---
|
||||
|
||||
## Модули
|
||||
|
||||
|
||||
| Модуль | Путь | Описание |
|
||||
|--------|------|----------|
|
||||
| `ring_buffer` | [ring_buffer/](ring_buffer/README.md) | SPSC кольцевой буфер байт, lock-free |
|
||||
| `log` | [log/](log/README.md) | Платформонезависимый логгер с callback-транспортом |
|
||||
|
|
|
|||
96
utils/log/README.md
Normal file
96
utils/log/README.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# utils/log
|
||||
|
||||
Платформонезависимый логгер с callback-транспортом.
|
||||
|
||||
Ядро логгера (`log.c` / `log.h`) не знает о конкретном транспорте — UART,
|
||||
USB CDC, Flash и т.д. Транспорт подключается через `log_init()` в виде
|
||||
callback-функции. Адаптеры живут в `port/log/`.
|
||||
|
||||
| Параметр | Значение |
|
||||
|---|---|
|
||||
| Формат | `[ timestamp][L][TAG] сообщение\r\n` |
|
||||
| Буфер строки | 256 байт (переопределяется через `LOG_BUF_SIZE`) |
|
||||
| Управление уровнем | `LOG_LEVEL` через CMake `-DLOG_LEVEL=N` |
|
||||
| Thread-safety | мьютекс через weak-хуки (`log_mutex_lock/unlock`) |
|
||||
| Зависимости | `<stdarg.h>`, `<stdio.h>`, `<stddef.h>` |
|
||||
|
||||
## Уровни
|
||||
|
||||
| N | Макрос | Имя |
|
||||
|---|--------|-----|
|
||||
| 0 | — | off — все `LOG_*` → `((void)0)`, нулевой ROM |
|
||||
| 1 | `LOG_E` | error |
|
||||
| 2 | `LOG_W` | warn |
|
||||
| 3 | `LOG_I` | info |
|
||||
| 4 | `LOG_D` | debug |
|
||||
| 5 | `LOG_V` | verbose |
|
||||
|
||||
По умолчанию: `VERBOSE` в Debug-сборке, `OFF` в Release (`NDEBUG`).
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```c
|
||||
// main.c — зарегистрировать транспорт
|
||||
#include "log/log.h"
|
||||
#include "port/log_uart.h"
|
||||
|
||||
log_uart_init(); // инициализировать адаптер транспорта
|
||||
log_init(uart_log_write, NULL);
|
||||
|
||||
// Любой .c файл
|
||||
#include "log/log.h"
|
||||
|
||||
LOG_I("BOOT", "Started, tick=%lu", (unsigned long) bsp_tick_get_ms());
|
||||
LOG_W("SDIO", "Card not detected");
|
||||
LOG_D("UART", "RX=%u bytes", bsp_uart_host_rx_available());
|
||||
LOG_E("CAN", "Bus-off, err=%d", err);
|
||||
```
|
||||
|
||||
Вывод:
|
||||
|
||||
```bash
|
||||
[ 1234][I][BOOT] Started, tick=1234
|
||||
[ 1235][W][SDIO] Card not detected
|
||||
```
|
||||
|
||||
## Транспортный адаптер
|
||||
|
||||
Адаптер — функция типа `log_write_cb_t`:
|
||||
|
||||
```c
|
||||
typedef void (*log_write_cb_t)(const char *p_buf, size_t len, void *p_ctx);
|
||||
```
|
||||
|
||||
Готовые адаптеры в `port/log/`:
|
||||
|
||||
| Адаптер | Транспорт |
|
||||
|---------|-----------|
|
||||
| `log_uart` | `bsp_uart_host` (LPUART1, MCU-Link VCOM) |
|
||||
|
||||
## Мьютекс и временна́я метка (FreeRTOS)
|
||||
|
||||
Для bare-metal ничего делать не нужно — weak-хуки по умолчанию NOP, временна́я метка возвращает 0.
|
||||
|
||||
Для FreeRTOS переопределить в одном `.c` файле прошивки:
|
||||
|
||||
```c
|
||||
// firmware/tft_app/src/log_os.c
|
||||
#include "log/log.h"
|
||||
#include "FreeRTOS.h"
|
||||
#include "semphr.h"
|
||||
#include "bsp/tick.h"
|
||||
|
||||
static SemaphoreHandle_t s_mutex;
|
||||
|
||||
void log_mutex_init(void) { s_mutex = xSemaphoreCreateMutex(); }
|
||||
void log_mutex_lock(void) { xSemaphoreTake(s_mutex, portMAX_DELAY); }
|
||||
void log_mutex_unlock(void) { xSemaphoreGive(s_mutex); }
|
||||
|
||||
uint32_t log_get_timestamp_ms(void) { return bsp_tick_get_ms(); }
|
||||
```
|
||||
|
||||
> ⚠️ `LOG_*` нельзя вызывать из ISR — если callback транспорта блокирующий.
|
||||
|
||||
## Тесты
|
||||
|
||||
`tests/host/log/` — host unit-тесты (Unity + fff).
|
||||
Loading…
Reference in a new issue