From 59b9ff55f7dec03177ae1f0906cb0b5185c3ea5e Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Fri, 27 Mar 2026 14:36:12 +0300 Subject: [PATCH] =?UTF-8?q?#=2014=20-=20=D0=A8=D0=B0=D0=B1=D0=BB=D0=BE?= =?UTF-8?q?=D0=BD=20HIL-=D1=82=D0=B5=D1=81=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Создан первый рабочий, шаблонный HIL-тест (bsp_opto) - актуализирована вся документация --- .env.example | 12 +- .gitignore | 1 + README.md | 214 +++----- bsp/README.md | 135 +++--- bsp/opto/README.md | 97 +++- bsp/opto/include/bsp/opto.h | 171 +++++-- bsp/opto/src/opto.c | 188 ++++++-- docs/CMAKE_HINTS.md | 219 --------- docs/DEV_ARCH.md | 455 +++++++++--------- docs/{ => hardware}/MCULINKINSERT.pdf | Bin .../board/MT48LCxxM4,8,16A2.pdf | Bin docs/{ => hardware}/board/schematic.pdf | Bin .../m5stamPLC/K141_sch_StamPLC_V10_CPU.pdf | Bin .../m5stamPLC/K141_sch_StamPLC_V10_IO.pdf | Bin docs/{ => mimxrt1052}/BOOT_FLAGS.md | 0 {tools/host => docs/mimxrt1052}/HAB_GUIDE.md | 0 docs/testing/hil/HIL_BENCH.md | 188 ++++++++ docs/testing/hil/HIL_CREATE_TEST.md | 374 ++++++++++++++ docs/testing/hil/HIL_HOW_TO.md | 204 ++++++++ .../testing/host}/HOST_CREATE_TEST.md | 0 firmware/test/main.c | 65 ++- tests/HIL_CREATE_TEST.md | 372 -------------- tests/host/mocks/fsl_gpio.h | 1 + tests/host/opto/test_bsp_opto.c | 307 ++++++++++-- tests/target/README.md | 32 -- tests/target/hil_opto/main.c | 19 +- tools/hil/README.md | 340 ++----------- tools/hil/conftest.py | 100 ++-- tools/hil/m5/agent.py | 13 +- tools/hil/m5/cli.py | 24 +- tools/hil/m5/firmware/README.md | 3 - tools/hil/test_opto.py | 181 +++++-- tools/host/README.md | 335 ++----------- utils/README.md | 8 +- utils/log/README.md | 96 ++++ 35 files changed, 2189 insertions(+), 1965 deletions(-) delete mode 100644 docs/CMAKE_HINTS.md rename docs/{ => hardware}/MCULINKINSERT.pdf (100%) rename docs/{ => hardware}/board/MT48LCxxM4,8,16A2.pdf (100%) rename docs/{ => hardware}/board/schematic.pdf (100%) rename docs/{ => hardware}/m5stamPLC/K141_sch_StamPLC_V10_CPU.pdf (100%) rename docs/{ => hardware}/m5stamPLC/K141_sch_StamPLC_V10_IO.pdf (100%) rename docs/{ => mimxrt1052}/BOOT_FLAGS.md (100%) rename {tools/host => docs/mimxrt1052}/HAB_GUIDE.md (100%) create mode 100644 docs/testing/hil/HIL_BENCH.md create mode 100644 docs/testing/hil/HIL_CREATE_TEST.md create mode 100644 docs/testing/hil/HIL_HOW_TO.md rename {tests => docs/testing/host}/HOST_CREATE_TEST.md (100%) delete mode 100644 tests/HIL_CREATE_TEST.md delete mode 100644 tests/target/README.md delete mode 100644 tools/hil/m5/firmware/README.md create mode 100644 utils/log/README.md diff --git a/.env.example b/.env.example index c9cdfc8..d99ca71 100644 --- a/.env.example +++ b/.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,17 +28,18 @@ 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 - + # FCB-бинарник для flash_swd.py (Flash Configuration Block, W25Q128 Quad SPI) 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 diff --git a/.gitignore b/.gitignore index 0c1f08a..1123361 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,7 @@ Debug/ Release/ build* Testing/ +!docs/testing !build.just # Файлы настройки среды разработки diff --git a/README.md b/README.md index b9f82b2..5b4e26a 100644 --- a/README.md +++ b/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//`) с текстовым CLI через UART и **pytest-тесты** (`tools/hil/test_.py`). pyOCD загружает `.elf` в RAM через MCU-Link (CMSIS-DAP), pytest общается с прошивкой через MCU-Link VCOM. +**HIL-тесты** — каждый тест это пара: C-прошивка с UART CLI (`tests/target//`) и pytest-файл (`tools/hil/test_.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 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 ``` diff --git a/bsp/README.md b/bsp/README.md index e8569d7..323961d 100644 --- a/bsp/README.md +++ b/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_`. -### 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 #include -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// ├── CMakeLists.txt +├── README.md ├── include/ │ └── bsp/ │ └── .h # публичный API — без NXP хедеров └── src/ - ├── .c # реализация - └── <конфиг>.h # приватные конфиги стека (напр. usb_device_config.h) + └── .c # реализация — fsl_*.h только здесь ``` ```cmake @@ -140,21 +127,19 @@ bsp// add_library(bsp_ STATIC src/.c) target_include_directories(bsp_ - PUBLIC include/ # bsp/.h доступен снаружи - PRIVATE src/ # конфиги и NXP хедеры — только внутри + PUBLIC include/ + PRIVATE src/ ) target_link_libraries(bsp_ - PUBLIC bsp_board # транзитивно во все потребители - PRIVATE sdk_ # NXP SDK — не торчит наружу + PUBLIC bsp_status + PRIVATE bsp_board sdk_ ) ``` ### Защита от 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// со структурой include/src/CMakeLists.txt -[ ] Публичный хедер include/bsp/.h — без NXP хедеров -[ ] target_link_libraries: PUBLIC bsp_board, PRIVATE sdk_* -[ ] Guard BUILD_TESTS_HOST в CMakeLists или stub-реализация в .c -[ ] add_subdirectory(bsp/) в bsp/CMakeLists.txt -[ ] Добавить target в нужные прошивки (firmware/*/CMakeLists.txt) +[ ] bsp//include/bsp/.h — публичный API без NXP хедеров +[ ] bsp//src/.c — реализация +[ ] bsp//CMakeLists.txt — guard BUILD_TESTS_HOST + зависимости +[ ] bsp//README.md — аппаратура + API + использование +[ ] bsp/CMakeLists.txt — add_subdirectory() +[ ] firmware/*/CMakeLists.txt — добавить bsp_ в нужные прошивки ``` diff --git a/bsp/opto/README.md b/bsp/opto/README.md index b24f33e..7d03998 100644 --- a/bsp/opto/README.md +++ b/bsp/opto/README.md @@ -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,16 +73,17 @@ 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 }, - .rs_as_gpio = false, - .debounce_ms = 10U, + .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, }; 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 может работать в двух режимах: diff --git a/bsp/opto/include/bsp/opto.h b/bsp/opto/include/bsp/opto.h index 12223e6..a31d9a7 100644 --- a/bsp/opto/include/bsp/opto.h +++ b/bsp/opto/include/bsp/opto.h @@ -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, - * .debounce_ms = 10U, + * .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 @@ -34,9 +64,9 @@ typedef enum { - BSP_OPTO_CH_IN1 = 0U, /* EXT_IN1 — всегда доступен */ - BSP_OPTO_CH_IN2, /* EXT_IN2 — всегда доступен */ - BSP_OPTO_CH_RS, /* RS_RX — только при rs_as_gpio == true */ + BSP_OPTO_CH_IN1 = 0U, /* EXT_IN1 — всегда доступен */ + BSP_OPTO_CH_IN2, /* EXT_IN2 — всегда доступен */ + BSP_OPTO_CH_RS, /* RS_RX — только при rs_as_gpio == true */ BSP_OPTO_CH_COUNT, } bsp_opto_ch_t; @@ -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; @@ -105,41 +170,49 @@ extern "C" #endif /** - * @brief Инициализировать модуль. - * - * Конфигурирует пины (pin mux), настраивает GPIO на вход, - * включает прерывания по обоим фронтам. Для RS_RX вызывает - * BOARD_InitRS_GPIO() если rs_as_gpio == true. - * - * @param p_config указатель на конфигурацию (не NULL) - * @return BSP_OK или BSP_ERR_INVALID_ARG - */ + * @brief Инициализировать модуль. + * + * Конфигурирует пины, настраивает GPIO на вход, включает прерывания. + * Для RS_RX вызывает BOARD_InitRS_GPIO() если rs_as_gpio == true. + * Начальное состояние каналов MODE_LEVEL читается с пинов при инициализации. + * + * @param p_config указатель на конфигурацию (не NULL) + * @return BSP_OK или BSP_ERR_PARAM + */ bsp_status_t bsp_opto_init(const bsp_opto_config_t *p_config); /** - * @brief Прочитать текущее подтверждённое состояние канала. - * - * Возвращает последнее состояние, зафиксированное после дебаунса. - * Может вызываться из любого контекста. - * - * @param ch номер канала - * @return BSP_OPTO_STATE_INACTIVE / BSP_OPTO_STATE_ACTIVE - */ + * @brief Прочитать текущее подтверждённое состояние канала (только MODE_LEVEL). + * + * Для MODE_PROTO всегда возвращает BSP_OPTO_STATE_INACTIVE — + * состояние RS отслеживается коллбэком из ISR. + * + * @param ch номер канала + * @return BSP_OPTO_STATE_INACTIVE / BSP_OPTO_STATE_ACTIVE + */ bsp_opto_state_t bsp_opto_read(bsp_opto_ch_t input_channel); /** - * @brief Обработать отложенные события дебаунса. - * - * Должна вызываться из main loop на каждой итерации. - * НЕ вызывать из ISR. - * - * Для каждого канала с pending-флагом: - * — если прошло >= debounce_ms с последнего фронта, - * перечитывает пин, сравнивает с confirmed_state, - * при изменении вызывает коллбэк и обновляет confirmed_state. - */ + * @brief Обработать отложенные события дебаунса (только MODE_LEVEL). + * + * Должна вызываться из 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 \ No newline at end of file diff --git a/bsp/opto/src/opto.c b/bsp/opto/src/opto.c index c89e7d1..71c4276 100644 --- a/bsp/opto/src/opto.c +++ b/bsp/opto/src/opto.c @@ -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,20 +35,21 @@ #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) /* ── Состояние канала ────────────────────────────────────────────────────── */ typedef struct { - volatile uint32_t last_edge_ms; /* bsp_tick_get_ms() в момент фронта */ - volatile bool pending; /* фронт зафиксирован, ждём дебаунс */ - volatile uint32_t raw_pin; /* raw состояние пина в момент фронта */ - bsp_opto_state_t confirmed_state; /* последнее подтверждённое состояние */ - uint8_t pin; /* номер пина в GPIO1 */ - bool enabled; /* канал активен */ + volatile uint32_t last_edge_ms; /* bsp_tick_get_ms() в момент фронта */ + volatile bool pending; /* фронт зафиксирован, ждём дебаунс */ + 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->raw_pin = read_pin(p_ch->pin); 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); } } } diff --git a/docs/CMAKE_HINTS.md b/docs/CMAKE_HINTS.md deleted file mode 100644 index a0bb217..0000000 --- a/docs/CMAKE_HINTS.md +++ /dev/null @@ -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 - │ - НЕТ ──→ (не добавляйте это свойство вообще) -``` - ---- diff --git a/docs/DEV_ARCH.md b/docs/DEV_ARCH.md index b31f756..669d51f 100644 --- a/docs/DEV_ARCH.md +++ b/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,81 +28,94 @@ ├── Хост (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-прошивки) -│ ├── cmake + ninja ← система сборки -│ ├── clang-17 ← компилятор для host-тестов -│ ├── clangd-17 ← LSP (автодополнение, диагностика) -│ ├── clang-tidy-17 ← статический анализ -│ ├── clang-format-17 ← форматирование кода -│ ├── just ← запуск задач внутри контейнера (just build::*) -│ ├── uv + spsdk ← сборка HAB-образов (только nxpimage) -│ └── Unity + fff ← фреймворки host-тестов +│ ├── arm-none-eabi-gcc ← кросс-компилятор (firmware + HIL target-прошивки) +│ ├── cmake + ninja ← система сборки +│ ├── clang-17 ← компилятор для host-тестов +│ ├── clangd-17 ← LSP (автодополнение, диагностика) +│ ├── clang-tidy-17 ← статический анализ +│ ├── clang-format-17 ← форматирование кода +│ ├── just ← запуск задач внутри контейнера (just build::*) +│ ├── uv + spsdk ← сборка HAB-образов (только nxpimage) +│ └── Unity + fff ← фреймворки host-тестов │ -└── Плата TFT (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} + └─▶ .vscode/launch.json ← через ${env:GDB_PORT} ``` -`.env.example` — шаблон без значений, коммитится в репозиторий. - --- ## 4. Структура automation-части репозитория @@ -128,37 +140,74 @@ HIL_VCOM_PORT=/dev/tty.usbmodemXXXX # VCOM-порт MCU-Link (macOS/Linux) ```bash / ├── justfile ← корневой оркестратор; модули: build, host, ci -├── bootstrap.sh ← уровень 0: устанавливает just+uv → just host::bootstrap -├── pyocd.yaml ← конфигурация pyOCD (target: cortex_m, RAM-режим) +├── bootstrap.sh ← уровень 0: устанавливает just + uv → just host::bootstrap +├── 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_, 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_, 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 ← 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 && cd 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 -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_` загружает 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//` и pytest-файл в `tools/hil/test_.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 -Пресет: target-debug → ram.ld → ITCM/DTCM -Запуск: just host::hil-run +Инструменты: pyOCD (SWD) + pyserial (UART) + pytest + M5StampPLC (реле) +Пресет: target-debug → ram.ld → ITCM/DTCM +Запуск: just host::hil-run (на хосте) ``` -Текущие тесты: `test_uart.py` — PING/ECHO/BUF_SIZE через `bsp_uart_host`. - -### 10.3 Будущие HIL-тесты (с M5StampPLC) - -Для тестов с внешними интерфейсами (CAN, GPIO, RS485) понадобится промежуточное звено: - -```bash -pytest → M5StampPLC (Arduino CLI) → CAN/GPIO → RT1052 -``` - -M5StampPLC реализует тот же текстовый CLI — pytest работает одинаково с обоими каналами. +Текущие тесты: `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 -``` diff --git a/docs/MCULINKINSERT.pdf b/docs/hardware/MCULINKINSERT.pdf similarity index 100% rename from docs/MCULINKINSERT.pdf rename to docs/hardware/MCULINKINSERT.pdf diff --git a/docs/board/MT48LCxxM4,8,16A2.pdf b/docs/hardware/board/MT48LCxxM4,8,16A2.pdf similarity index 100% rename from docs/board/MT48LCxxM4,8,16A2.pdf rename to docs/hardware/board/MT48LCxxM4,8,16A2.pdf diff --git a/docs/board/schematic.pdf b/docs/hardware/board/schematic.pdf similarity index 100% rename from docs/board/schematic.pdf rename to docs/hardware/board/schematic.pdf diff --git a/docs/m5stamPLC/K141_sch_StamPLC_V10_CPU.pdf b/docs/hardware/m5stamPLC/K141_sch_StamPLC_V10_CPU.pdf similarity index 100% rename from docs/m5stamPLC/K141_sch_StamPLC_V10_CPU.pdf rename to docs/hardware/m5stamPLC/K141_sch_StamPLC_V10_CPU.pdf diff --git a/docs/m5stamPLC/K141_sch_StamPLC_V10_IO.pdf b/docs/hardware/m5stamPLC/K141_sch_StamPLC_V10_IO.pdf similarity index 100% rename from docs/m5stamPLC/K141_sch_StamPLC_V10_IO.pdf rename to docs/hardware/m5stamPLC/K141_sch_StamPLC_V10_IO.pdf diff --git a/docs/BOOT_FLAGS.md b/docs/mimxrt1052/BOOT_FLAGS.md similarity index 100% rename from docs/BOOT_FLAGS.md rename to docs/mimxrt1052/BOOT_FLAGS.md diff --git a/tools/host/HAB_GUIDE.md b/docs/mimxrt1052/HAB_GUIDE.md similarity index 100% rename from tools/host/HAB_GUIDE.md rename to docs/mimxrt1052/HAB_GUIDE.md diff --git a/docs/testing/hil/HIL_BENCH.md b/docs/testing/hil/HIL_BENCH.md new file mode 100644 index 0000000..a1b3916 --- /dev/null +++ b/docs/testing/hil/HIL_BENCH.md @@ -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_ (scope=module, зависит от m5) + └── pyOCD: FLEXRAM → load ELF → run_from_vectors + +фикстура uart_ (scope=module, зависит от loaded_) + └── открыть 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. Обновить таблицу подключений выше. diff --git a/docs/testing/hil/HIL_CREATE_TEST.md b/docs/testing/hil/HIL_CREATE_TEST.md new file mode 100644 index 0000000..0355055 --- /dev/null +++ b/docs/testing/hil/HIL_CREATE_TEST.md @@ -0,0 +1,374 @@ +# Добавление нового HIL-теста + +## Обзор стека + +```bash +devcontainer хост +───────────────────────────────── ──────────────────────────────────── +tests/target// tools/hil/ + main.c ← C-прошивка с CLI test_.py ← pytest-тесты + CMakeLists.txt conftest.py ← фикстуры (общие) + m5/agent.py ← агент M5 (если нужен) +CMakePresets.json + target-debug-build just/host.just + └── targets: [test_] hil-run, hil- + +just/build.just + build-hil +``` + +Два типа тестов: + +| Тип | Использует M5 | Когда применять | +|-----|--------------|-----------------| +| **Базовый** | Нет | Тестирование UART CLI, алгоритмов, таймингов | +| **С M5** | Да | Тестирование GPIO, оптовходов, реле, питания | + +--- + +## Шаг 1 — C-прошивка: `tests/target//` + +### `main.c` — шаблон + +```c +#include "board.h" +#include "bsp/led.h" +#include "bsp/tick.h" +#include "bsp/uart_host.h" +#include + +#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_) + +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} $ + COMMENT "Size: ${TARGET_NAME}") +``` + +--- + +## Шаг 2 — Подключить в `tests/target/CMakeLists.txt` + +```cmake +add_subdirectory(host_uart) +add_subdirectory(hil_opto) +add_subdirectory() # ← добавить строку +``` + +--- + +## Шаг 3 — `CMakePresets.json`: добавить таргет + +```json +{ + "name": "target-debug-build", + "configurePreset": "target-debug", + "targets": [ + "test_host_uart", + "test_hil_opto", + "test_" + ] +} +``` + +--- + +## Шаг 4 — Сборка + +```bash +# В devcontainer: +just build::build-hil + +# Проверить что новый таргет собрался: +ls build/target-debug/tests/target//test_.elf +``` + +--- + +## Шаг 5 — `conftest.py`: добавить фикстуры + +Открыть `tools/hil/conftest.py` и добавить в конец раздела с фикстурами загрузки. + +### Базовый тест (без M5) + +```python +@pytest.fixture(scope="module") +def loaded_(request: pytest.FixtureRequest) -> None: + _load_elf( + request, + Path(cfg.BUILD_DIR) / "tests/target//test_.elf", + ) + +@pytest.fixture(scope="module") +def uart_( + request: pytest.FixtureRequest, + loaded_, # ← гарантирует порядок: 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_(request: pytest.FixtureRequest, m5: M5Agent) -> None: + """ + Зависит от m5 — питание таргета уже включено к моменту загрузки ELF. + """ + _load_elf( + request, + Path(cfg.BUILD_DIR) / "tests/target//test_.elf", + ) + +@pytest.fixture(scope="module") +def uart_( + request: pytest.FixtureRequest, + loaded_, +) -> Generator[serial.Serial, None, None]: + ser = _open_uart_and_wait_ready(request) + yield ser + ser.close() +``` + +**Правило:** если тест управляет железом через M5 — `loaded_` должен явно +зависеть от `m5`. Это гарантирует что питание включено до того как pyOCD +попытается подключиться к MCU. + +--- + +## Шаг 6 — `tools/hil/test_.py` + +### Базовый тест (без M5) + +```python +"""test_.py — HIL тест <что тестируем>.""" +import pytest +from conftest import uart_cmd + + +class Test: + + @pytest.fixture(autouse=True) + def _setup(self, uart_): + self.ser = uart_ + + def test_ping(self): + """Базовая проверка канала.""" + assert uart_cmd(self.ser, "PING") == "PONG" + + def test_something(self): + resp = uart_cmd(self.ser, "MY_CMD") + assert resp == "EXPECTED" +``` + +### Тест с M5 + +```python +"""test_.py — HIL тест <что тестируем> через M5StampPLC.""" +import time +import pytest +from conftest import uart_cmd + +SETTLE_S = 0.15 # ждать после переключения реле + + +class Test: + + @pytest.fixture(autouse=True) + def _setup(self, uart_, m5): + self.ser = uart_ + 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-тест ')] +[group('hil')] +hil-: + HIL_BUILD_DIR={{ _hil_build }} \ + uv run --directory {{ HIL_DIR }} pytest test_.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- + +# 4. хост — загрузить ELF вручную без тестов (для отладки) +uv run --directory tools/hil python load_and_run.py \ + build/target-debug/tests/target//test_.elf + +# 5. хост — запустить один тест +uv run --directory tools/hil pytest test_.py::Test::test_ping -v +``` + +--- + +## Как работают фикстуры + +### Цепочка зависимостей + +```bash +test_foo() + └── _setup (function scope, autouse) + ├── uart_ (module scope) ← открыт один раз на весь файл + │ └── loaded_ ← ELF загружен один раз + │ └── m5 ← (если нужен) питание включено + └── m5 (module scope) ← (если нужен напрямую в тесте) +``` + +`scope=module` — фикстура создаётся один раз на весь тест-файл, уничтожается +после последнего теста. ELF грузится один раз, порт открывается один раз. + +### Порядок при запуске нескольких файлов + +```bash +pytest test_uart.py test_.py + +test_uart.py test_.py +───────────────────── ───────────────────── +loaded_host_uart m5 ← создаётся +uart ← создаётся loaded_ + test_ping uart_ ← создаётся + test_echo test_ping +uart.close() test_something + uart_.close() + m5 teardown → power(False) +``` + +Каждый файл — своя загрузка ELF, свой UART-сеанс. MCU перезагружается между файлами. + +--- + +## Чеклист + +```bash +[ ] tests/target//main.c — C-прошивка с CLI + READY-паттерн +[ ] tests/target//CMakeLists.txt — сборка с bsp_boot_ram +[ ] tests/target/CMakeLists.txt — add_subdirectory() +[ ] CMakePresets.json — добавить test_ в targets +[ ] tools/hil/conftest.py — loaded_ + uart_ +[ ] tools/hil/test_.py — pytest-тесты +[ ] just/host.just — рецепт hil- (опционально) +[ ] just build::build-hil — зелёная сборка +[ ] just host::hil- — зелёный прогон +``` diff --git a/docs/testing/hil/HIL_HOW_TO.md b/docs/testing/hil/HIL_HOW_TO.md new file mode 100644 index 0000000..26e9255 --- /dev/null +++ b/docs/testing/hil/HIL_HOW_TO.md @@ -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, ...} +``` diff --git a/tests/HOST_CREATE_TEST.md b/docs/testing/host/HOST_CREATE_TEST.md similarity index 100% rename from tests/HOST_CREATE_TEST.md rename to docs/testing/host/HOST_CREATE_TEST.md diff --git a/firmware/test/main.c b/firmware/test/main.c index a2dff39..34f8082 100644 --- a/firmware/test/main.c +++ b/firmware/test/main.c @@ -10,28 +10,35 @@ #include #include -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); } } diff --git a/tests/HIL_CREATE_TEST.md b/tests/HIL_CREATE_TEST.md deleted file mode 100644 index 63c0d87..0000000 --- a/tests/HIL_CREATE_TEST.md +++ /dev/null @@ -1,372 +0,0 @@ -# Добавление нового HIL-теста - -## Обзор стека - -```bash -devcontainer хост -──────────────────────────────── ────────────────────────────────────── -tests/target// tools/hil/ - main.c ← C-прошивка test_.py ← pytest-тесты - CMakeLists.txt conftest.py ← фикстуры (общие) - -CMakePresets.json just/host.just - target-debug-build hil-run, hil-smoke ... - └── targets: [test_] - -just/build.just - build-hil -``` - ---- - -## Шаг 1 — C-прошивка: `tests/target//` - -### `main.c` - -Минимальный шаблон для нового теста: - -```c -#include "board.h" -#include "bsp/led.h" -#include "bsp/tick.h" -#include "bsp/uart_host.h" -#include - -#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_) - -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} $ - COMMENT "Size: ${TARGET_NAME}" -) -``` - ---- - -## Шаг 2 — Подключить в `tests/target/CMakeLists.txt` - -```cmake -add_subdirectory(host_uart) -add_subdirectory() # ← добавить строку -``` - ---- - -## Шаг 3 — `CMakePresets.json`: добавить таргет в `target-debug-build` - -```json -{ - "name": "target-debug-build", - "configurePreset": "target-debug", - "targets": [ - "test_host_uart", - "test_" - ] -} -``` - ---- - -## Шаг 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_.py` - -```python -"""test_.py — HIL тест <что тестируем>.""" -import pytest -from conftest import uart_cmd - - -class Test: - - @pytest.fixture(autouse=True) - def _setup(self, loaded_, 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_(request: pytest.FixtureRequest) -> None: - _load_elf( - request, - Path(cfg.BUILD_DIR) / "tests/target//test_.elf", - ) -``` - -Фикстура `uart` уже зависит от `loaded_host_uart`. Для нового теста нужна -своя пара: `loaded_` + при необходимости своя `uart_` если нужен -отдельный порт или скорость. Обычно достаточно переиспользовать `uart`. - ---- - -## Шаг 7 — `just/host.just`: добавить рецепты (опционально) - -Для часто используемых тестов удобно добавить алиасы: - -```just -[group('hil')] -hil-run-: - HIL_BUILD_DIR={{_hil_build}} \ - uv run --directory {{HIL_DIR}} pytest test_.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- - -# 4. хост — загрузить ELF без тестов (для ручной отладки) -uv run --directory tools/hil python load_and_run.py \ - build/target-debug/tests/target//test_.elf -``` - ---- - -## Как работают фикстуры - -### Общая схема зависимостей - -Для каждого тест-модуля цепочка фикстур одна и та же: - -```bash -test_foo() - └── _setup (scope=function, autouse) - ├── loaded_ (scope=module) ← грузит ELF на MCU - └── uart (scope=module) ← открывает порт, ждёт READY - └── depends_on: loaded_ ← гарантирует порядок -``` - -`scope=module` означает: фикстура создаётся один раз на весь файл с тестами -и уничтожается после последнего теста в нём. Все тесты внутри одного файла -разделяют один и тот же экземпляр — ELF загружается один раз, порт открывается -один раз. - -### Порядок вызовов внутри одного модуля - -```bash -──────────────────────────────────── module scope (один раз на файл) - -1. loaded_() - └── _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_, создаётся после) - ├── 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_ teardown → (нет, возвращает None) -``` - -### Почему `uart` явно зависит от `loaded_` - -```python -def uart(request, loaded_): # ← зависимость объявлена в сигнатуре - ... -``` - -Без этой зависимости 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_`. -Всё остальное (`uart`, `uart_cmd`, `open_target`, `flexram_init`) переиспользуется: - -```python -# conftest.py — добавить: - -@pytest.fixture(scope="module") -def loaded_(request: pytest.FixtureRequest) -> None: - _load_elf( - request, - Path(cfg.BUILD_DIR) / "tests/target//test_.elf", - ) - -# Если нужна отдельная uart-фикстура (другой порт, другой бод): -@pytest.fixture(scope="module") -def uart_( - request: pytest.FixtureRequest, - loaded_, # ← порядок гарантирован -) -> 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_` не нужна — стандартная `uart` -работает для любого теста, потому что протокол (`READY` + текстовые команды) -одинаковый для всех прошивок. - ---- - -## Чеклист - -```bash -[ ] tests/target//main.c — C-прошивка с CLI -[ ] tests/target//CMakeLists.txt — сборка с bsp_boot_ram -[ ] tests/target/CMakeLists.txt — add_subdirectory() -[ ] CMakePresets.json — добавить test_ в targets -[ ] tools/hil/test_.py — pytest-тесты -[ ] tools/hil/conftest.py — добавить loaded_ фикстуру -[ ] just/host.just — алиас hil-run- (опционально) -``` diff --git a/tests/host/mocks/fsl_gpio.h b/tests/host/mocks/fsl_gpio.h index ccda5c0..c2ac53b 100644 --- a/tests/host/mocks/fsl_gpio.h +++ b/tests/host/mocks/fsl_gpio.h @@ -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); diff --git a/tests/host/opto/test_bsp_opto.c b/tests/host/opto/test_bsp_opto.c index 51bda24..9bff3fe 100644 --- a/tests/host/opto/test_bsp_opto.c +++ b/tests/host/opto/test_bsp_opto.c @@ -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) { - bsp_opto_config_t cfg = make_default_cfg(); - cfg.edges[BSP_OPTO_CH_IN1] = BSP_OPTO_EDGE_RISING; + GPIO_PinRead_fake.return_val = 0U; /* INACTIVE */ + bsp_opto_config_t cfg = make_default_cfg(); 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) { - bsp_opto_config_t cfg = make_default_cfg(); - cfg.edges[BSP_OPTO_CH_IN1] = BSP_OPTO_EDGE_FALLING; + GPIO_PinRead_fake.return_val = 1U; /* ACTIVE */ + bsp_opto_config_t cfg = make_default_cfg(); 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(); } \ No newline at end of file diff --git a/tests/target/README.md b/tests/target/README.md deleted file mode 100644 index 455f812..0000000 --- a/tests/target/README.md +++ /dev/null @@ -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** | diff --git a/tests/target/hil_opto/main.c b/tests/target/hil_opto/main.c index 1ea48da..619a6f8 100644 --- a/tests/target/hil_opto/main.c +++ b/tests/target/hil_opto/main.c @@ -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 }, + .callbacks = { opto_callback, opto_callback, opto_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_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); @@ -173,4 +182,4 @@ int main(void) } return 0; -} +} \ No newline at end of file diff --git a/tools/hil/README.md b/tools/hil/README.md index f455d8c..c36335a 100644 --- a/tools/hil/README.md +++ b/tools/hil/README.md @@ -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_, uart_) +├── 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//main.c ← C-прошивка с текстовым CLI через UART -tools/hil/test_.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_() - ├── 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_` - -```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//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//main.c — C-прошивка с CLI -2. tests/target//CMakeLists.txt — сборка с bsp_boot_ram -3. tests/target/CMakeLists.txt — add_subdirectory() -4. CMakePresets.json — добавить test_ в target-debug-build -5. tools/hil/test_.py — pytest-тесты -6. tools/hil/conftest.py — добавить loaded_ фикстуру -``` - -Фикстура для нового теста в `conftest.py`: - -```python -@pytest.fixture(scope="module") -def loaded_(request: pytest.FixtureRequest) -> None: - _load_elf( - request, - Path(cfg.BUILD_DIR) / "tests/target//test_.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. diff --git a/tools/hil/conftest.py b/tools/hil/conftest.py index 213babe..9a89981 100644 --- a/tools/hil/conftest.py +++ b/tools/hil/conftest.py @@ -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() + + # --------------------------------------------------------------------------- # Утилита для тестов # --------------------------------------------------------------------------- @@ -250,4 +260,4 @@ def uart_cmd(ser: serial.Serial, cmd: str) -> str: resp = ser.readline() if not resp: raise TimeoutError(f"Нет ответа на команду '{cmd}'") - return resp.decode("ascii", errors="replace").strip() + return resp.decode("ascii", errors="replace").strip() \ No newline at end of file diff --git a/tools/hil/m5/agent.py b/tools/hil/m5/agent.py index 2228d02..c1e90e3 100644 --- a/tools/hil/m5/agent.py +++ b/tools/hil/m5/agent.py @@ -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 — регистры @@ -432,4 +435,4 @@ def main() -> None: sys.stdout.write(json.dumps(resp) + "\r\n") -main() +main() \ No newline at end of file diff --git a/tools/hil/m5/cli.py b/tools/hil/m5/cli.py index ff3cb16..837b563 100644 --- a/tools/hil/m5/cli.py +++ b/tools/hil/m5/cli.py @@ -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) diff --git a/tools/hil/m5/firmware/README.md b/tools/hil/m5/firmware/README.md deleted file mode 100644 index 2a40fa6..0000000 --- a/tools/hil/m5/firmware/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Загрузчики с microPython - -> На HIL-стенд m5stamPLC необходимо предварительно загрузить интерпретор `microPython`. \ No newline at end of file diff --git a/tools/hil/test_opto.py b/tools/hil/test_opto.py index 067e5a6..3e391f3 100644 --- a/tools/hil/test_opto.py +++ b/tools/hil/test_opto.py @@ -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}" \ No newline at end of file diff --git a/tools/host/README.md b/tools/host/README.md index 92b4e4c..ffdc5dc 100644 --- a/tools/host/README.md +++ b/tools/host/README.md @@ -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` | — | `/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 ``` diff --git a/utils/README.md b/utils/README.md index 52dd42c..1b575f8 100644 --- a/utils/README.md +++ b/utils/README.md @@ -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-транспортом | diff --git a/utils/log/README.md b/utils/log/README.md new file mode 100644 index 0000000..ebf1a38 --- /dev/null +++ b/utils/log/README.md @@ -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`) | +| Зависимости | ``, ``, `` | + +## Уровни + +| 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).