diff --git a/CHANGELOG.md b/CHANGELOG.md index 722f13e..753c485 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,86 +55,153 @@ - Проверять, остаётся ли `.github/workflows/ci.yml` только сборочным workflow через `just ci::build` или начинает запускать host-тесты. - Отслеживать появление self-hosted runner, регулярных аппаратных прогонов или отчётов по HIL. - Проверять новые firmware-test модули в `firmware/test/src/tests/` и синхронные обновления протокола/документации. -- Следить за развитием BSP: MQS, RGB, bootloader или `tft_app`. +- Следить за развитием BSP: RGB (частично закрыто display-тестом), bootloader или `tft_app` — обе директории всё ещё не заведены. - Отслеживать локальные патчи поверх vendor SDK, которые нужно вести отдельным patch log. +- Отслеживать мерж ветки `feature-tui-monolith` в `dev` — после мержа эту запись нужно закрыть датой и финальным диапазоном SHA. -## [Не выпущено] — service-tui: кастомная прошивка нестандартной памяти (после слияния `feature-tui-python`) +## [Не выпущено] — service-tui: монолитный spsdk-бэкенд, устойчивость к обрыву USB, упаковка PyInstaller -Диапазон: `<заполнить после merge>..<текущий HEAD>` -Сравнение: `<заполнить после merge>` +Диапазон: `1801f1beb959d610d31ee3dcd1f91046953117d4..22c40779ef0ec9911031d7a5272c4611b596d3e8` + незакоммиченные изменения рабочего дерева (документация) +Сравнение: -> Изменения внесены **поверх** слияния `feature-tui-python → dev` — базовая -> архитектура `service-tui` (экраны, USB/M5-клиенты, оркестратор) приходит -> самим merge-коммитом; здесь только то, что было доработано отдельно после. +> Ветка `feature-tui-monolith` (от `dev`, поверх мержа `feature-tui-python`). +> **Полностью заменяет предыдущую версию этой записи**: прошивка сторонних +> бинарников через subprocess (`tools/host/flash_usb.py --fcb-path` + +> `nxpimage`) была реализацией на момент Фазы 0/раннего мержа и с тех пор +> заменена прямыми вызовами `spsdk` Python API — ничего из старой записи +> больше не описывает текущий код. ### Кратко -- `service-tui` теперь умеет прошивать сторонние/легаси бинарники (платы - с W25Q256/512 вместо штатного W25Q128) через USB SDP, с явной записью - FCB вместо ненадёжного для таких чипов auto-config Flashloader. -- Выбор оператора на `FlashScreen` (файл/память/DCD) запоминается на весь - запуск TUI — ускоряет прошивку партии одинаковых плат. -- Документация (`tools/production/README.md`, `tools/production/DEV_ARCH.md`, - `tools/host/README.md`, `docs/HOW_TO_FLASH.md`, `docs/DEV_ARCH.md`) - синхронизирована с фактическим состоянием кода. +- Прошивка в `service-tui` переведена с subprocess-обёртки над + `nxpimage`/`sdphost`/`blhost` на прямые вызовы `spsdk` Python API + (`McuBoot`/`SDP`/`HabImage`) — `app/flash_backend.py`, провалидировано + byte-exact на живом железе (macOS + Windows). `tools/host/flash_usb.py` + остаётся отдельным dev-CLI для `just host::flash*`, TUI его больше не + вызывает ни субпроцессом, ни как библиотеку. +- Обрыв USB во время прошивки/chip erase теперь надёжно типизируется во + всех трёх наблюдавшихся на железе сценариях (`SPSDKConnectionError`, + `SPSDKTimeoutError`, `False`-по-таймауту без исключения) и даёт оператору + единое понятное сообщение вместо «Непредвиденная ошибка». +- Собран первый standalone-бандл (PyInstaller, onedir) — alpha, вручную + протестирован на macOS и Windows. +- Документация (`tools/production/README.md`+`docs/DEV_ARCH.md`, корневые + `docs/*`, все `bsp/*/README.md`, корневой `README.md`) синхронизирована + с фактическим состоянием кода после всех фаз миграции. ### Добавлено -- `tools/host/flash_usb.py` — `write_fcb_explicit()` + флаг `--fcb-path`: - буквальная запись 512-байтного FCB-блоба (`write-memory 0x60000000`) - вместо magic option word `0xF000000F`. Штатный `--firmware`-путь - (`firmware_test`/`bootloader`/`app`) не тронут — работает как раньше. -- `tools/production/app/models.py` — `FcbVariant` (`W25Q128` покрывает и - W25Q64, `W25Q512` — и W25Q256) и `FlashPreset` (липкий выбор оператора). -- `tools/production/app/flasher.py` — `_build_custom_hab()`: сборка - HAB-образа на лету через `nxpimage hab export` из «сырого» бинарника - (без FCB/IVT/DCD) в `custom_binaries/`, с опциональным `DCDFilePath`; - стриминг вывода `nxpimage` в UI-лог, а не только в `logger.debug`. - `list_custom_binaries()` + `SERVICE_CUSTOM_BINARIES_DIR` — резолв - директории кастомных бинарей (внешняя, не пакуется в PyInstaller). -- `tools/production/app/screens/flash.py` — `Select` по `custom_binaries/`, - `Select` по `FcbVariant`, `Switch` DCD вместо свободного текстового - `Input`; предзаполнение из `FlashPreset` при создании экрана. -- `tools/production/app/app.py` — `ServiceApp._last_flash_preset`, - прокидывается в новый `FlashScreen` при каждом `DeviceDetected(FLASHING)`. +- `tools/production/app/flash_backend.py` — синхронное ядро прошивки на + spsdk: `detect_sdp`/`detect_cdc`, `load_flashloader`, `flash`, + `erase_chip`, `build_custom_hab` (`HabImage` вместо `nxpimage` CLI), + `write_fcb_explicit`/`write_fcb_auto`. Zero Textual/asyncio импортов, + тестируется без event loop. +- `tools/production/app/usb_ports.py` — `resolve_serial_port()` по VID:PID + (имя порта не переносимо между перевтыкиваниями). +- Иерархия `FlashBackendError`/`ConnectionLostError`/`DeviceNotFoundError`/ + `FlashLoaderTimeoutError`/`HabBuildError` с полем `connection_lost` — + различает физический обрыв USB от логической ошибки прошивки без + парсинга текста сообщения. +- `tools/production/tests/test_flash_backend.py` — вырос до 45 unit-тестов + backend'а, включая обе ветки обрыва USB (`SPSDKTimeoutError`, + `False`-по-таймауту + вариант B через `detect_sdp()`) и golden-тест + byte-exact сборки HAB. +- Кнопка «✕ Выйти из приложения» на `WaitingScreen`. +- `tools/production/service_tui.spec` — PyInstaller spec (onedir). +- `tools/production/docs/RELEASE_ROADMAP.md` — дорожная карта Фаз + 4a→4b→5→6 с принятыми решениями (Р10–Р12) и статусом гейтов. ### Изменено -- `tools/host/flash_usb.py`, `erase_chip()` — таймаут `blhost` - `flash-erase-all` увеличен до `-t 200000` (W25Q512 стирается заметно - дольше W25Q128, дефолтного таймаута не хватало). `flash-erase-region` - (обычная прошивка) не тронут — там стирается пара секторов, масштаб иной. -- `tools/production/app/app.tcss` — `#flash-target-group` ограничен по - высоте (`max-height: 18`, свой скролл), `#flash-log` защищён - `min-height: 6` — разросшаяся custom-группа больше не сжимает лог - прошивки до нечитаемого состояния. +- `tools/production/app/flasher.py` — переведён с subprocess + (`flash_usb.py` через `uv run`) на `asyncio.to_thread`-обёртку над + `flash_backend.py`; сборка кастомного HAB — через `HabImage` в отдельном + потоке, а не subprocess `nxpimage`. +- `tools/production/app/main.py` — логирование: root по умолчанию `INFO` + (было `DEBUG`), `spsdk`/`libusbsio` принудительно приглушены до + `WARNING` независимо от root; полный DEBUG — через + `SERVICE_LOG_LEVEL=DEBUG`. +- `tools/production/app/screens/flash.py` — троттлинг записи в + `#flash-log` для фазы `write` (раз на 10%, ~10 строк вместо ~135) без + потери плавности прогресс-бара. +- `bsp/sd/src/sd.c` — `bsp_sd_init()`/`bsp_sd_deinit()` теперь делают + аппаратный `USDHC_Reset()` + полный `memset(&g_sd, ...)` перед + повторной инициализацией: без этого non-blocking host driver SDK мог + оставаться в состоянии ожидания транзакции от предыдущей + diagnostic-сессии, и следующий `f_mount()` в тесте `usd` блокировался + навсегда. + +### Исправлено + +- Обёртка обрыва USB расширена с `SPSDKConnectionError` на + `(SPSDKConnectionError, SPSDKTimeoutError)` — второй тип не наследует + первый, но реально прилетает на read-фазе после write. +- Вариант B для команд, возвращающих `False` без исключения + (`flash_erase_all`/`flash_erase_region`/`write_memory`): при `False` + выполняется быстрый `detect_sdp()` — устройство пропало с шины → + `ConnectionLostError`, устройство на месте → обычная `FlashBackendError`. +- Баг «File not found» для bootloader/app/firmware_test при резолве путей + прошивки (Фаза 4a). +- Unit-тест моки (`test_cli.c`, `test_bsp_can.c`, `test_firmware_runner.c`, + stub-хедеры `fsl_clock.h`/`version.h`) — фиксы после рефакторинга + `cli.c`/`test_runner.c`. + +### Тесты + +- `test_flash_backend.py` — вырос до 45 тестов, включая гейт по + `SPSDKTimeoutError` и переклассификации erase-таймаута (вариант B). ### Документация -- `tools/production/README.md` — мокап `FlashScreen` под факт (Select/Select/ - Switch), новый workflow «Прошивка стороннего бинарника», `SERVICE_CUSTOM_BINARIES_DIR` - в примере `.env`. -- `tools/production/DEV_ARCH.md` — новый §8 (конвейер кастомной прошивки, - `FlashPreset`, явная запись FCB, обоснование отказа от auto-config для - W25Q256/512 и от полноценного авто-батч-режима прошивки). -- `docs/HOW_TO_FLASH.md` — §1.5, сноска в сравнительной таблице способов - прошивки (FCB «не нужен» верно только для W25Q128). -- `docs/DEV_ARCH.md` — `tools/production/` добавлен в дерево структуры - репозитория (отсутствовал ранее). -- `tools/host/README.md` — актуализирован статус `dcd/*.bin` (`w25q512_fdcb.bin` - теперь используется), указатель на `service-tui` как способ прошивки - нестандартной памяти. +- `tools/production/README.md`/`tools/production/docs/DEV_ARCH.md` — + полностью пересмотрены под факт: убраны все следы subprocess/`nxpimage`/ + `flash_usb.py` из описания архитектуры прошивки; добавлены §6.2 + (обработка обрыва USB), §14 (PyInstaller/frozen-резолв путей), §15 + (логирование); зафиксирован разрыв между закоммиченным + `service_tui.spec` (`datas` только `../shared`) и фактическим + содержимым уже собранных релизных бандлов в `dist/`. +- `docs/testing/PROTOCOL.md` — версия `0.1.0`→`0.1.2`, добавлена команда + `get_version` и события `test_list`/`uid_response`/`version_response`, + матрица тестов исправлена (убраны никогда не существовавшие `uart_ttl`/ + `uart_iso`, добавлен реальный `mqs`), поток Display дополнен шагами + ротации (`display_rot0`/`display_rot_base`). +- `docs/testing/host/HOST_CREATE_TEST.md` — был байт-в-байт дубликатом + `docs/HOW_TO_DEBUG.md` (копипаст-баг, минимум с 2026-06-23); переписан + как реальный гайд по добавлению host-теста. +- `docs/HOW_TO_FLASH.md` (§1.5 под факт spsdk-конвейера), `docs/DEV_ARCH.md` + (в дереве `tools/hil/` недоставало `04_test_button.py`), + `docs/testing/hil/HIL_CREATE_TEST.md` (пример `loaded_` без `m5` + вводил в заблуждение — питание таргета всегда идёт через M5, не только + сигнальные реле) — актуализированы. +- `bsp/usb_cdc/README.md` (VID/PID был заявлен как заглушка `0x1234:0x0001`, + реально прошит `0x1996:0x00AD`), `bsp/uart_host/README.md` (в списке API + отсутствовали реальные `bsp_uart_host_deinit/rx_available/rx_flush`), + `bsp/can/README.md` (несуществующие в коде `bsp_can.c`/`can_mock.h`/ + `bsp_can_rx_cb_t`) — исправлены по сверке с заголовками. +- `bsp/mqs/{mqs.c,mqs.h,mqs_amp.c}` — докстринги приведены в соответствие + с кодом (были «SAI1»/«16 кГц», реально SAI3/12 кГц — подтверждено + сверкой с `bsp/generated/clock_config.c`); `bsp/provisioning/provisioning.h` + — докстринг порядка байт UID исправлен на соответствующий реализации + (`provisioning.c` пишет CFG0 первым, докстринг утверждал обратное). +- Корневой `README.md` — `firmware/bootloader/`/`firmware/tft_app/` + помечены как запланированные, а не готовые (директорий не существует, + `add_subdirectory()` закомментирован в корневом `CMakeLists.txt`); + добавлен ранее отсутствовавший раздел «Инструменты (`tools/`)» — + `tools/production/` (service-tui) нигде не упоминался. ### Известные ограничения -- Auto-config Flashloader для W25Q256/512 не проверялся напрямую — решение - писать FCB явно снимает вопрос архитектурно, но не подтверждает и не - опровергает надёжность auto-config как таковую. -- Полноценный режим массового программирования (авто-прошивка по факту - детекта USB, без подтверждения оператора) рассмотрен и отклонён — в - SDP/Flashloader-режиме нет способа прочитать UID платы для идентификации. -- Standalone-упаковка (`PyInstaller`) для этого функционала ещё не - реализована — см. `tools/production/RELEASE_PLAN.md`. +- `service_tui.spec` не включает `datas` для `spsdk`/`dcd/*.bin`/ + `pyproject.toml`, хотя уже собранные alpha-бандлы их содержат — спек + нужно синхронизировать перед следующей сборкой релиза. +- `pyusb` в `pyproject.toml` — мёртвая зависимость (Р7 перевёл детект на + `spsdk`/`serial.tools.list_ports`), кандидат на удаление. +- Auto-config Flashloader для W25Q256/512 не проверялся напрямую — решили + не полагаться на него вообще, FCB для кастомных бинарей всегда пишется + явно. +- Массовое программирование (авто-прошивка по факту детекта SDP, без + подтверждения оператора) рассмотрено и отклонено — в SDP/Flashloader-режиме + нет способа прочитать UID платы для идентификации. ## [2026-06-29] — Этапы 6г–7: MQS, HIL pytest firmware_test, Provisioning diff --git a/README.md b/README.md index a79e6d8..0ef62d4 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,21 @@ | Проект | Путь | Описание | | ------------------- | ---------------------- | -------------------------------------------------------------------------------------- | -| Тестовая прошивка | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS | -| Загрузчик | `firmware/bootloader/` | A/B обновление через uSD. Обновляется только через USB ROM + blhost / SWD | -| Production прошивка | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком | +| Тестовая прошивка (✅ реализована) | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS | +| Загрузчик (⏳ запланирован) | `firmware/bootloader/` | A/B обновление через uSD. Обновляется только через USB ROM + blhost / SWD | +| Production прошивка (⏳ запланирован) | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком | + +`bootloader`/`tft_app` пока не реализованы. + +--- + +## Инструменты (`tools/`) + +| Инструмент | Путь | Назначение | +| ------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------- | +| Сервисный TUI | `tools/production/` | Диагностика и прошивка готовых плат сервисным инженером (Textual, standalone-бинарь). [README](tools/production/README.md) | +| Прошивка (dev-CLI) | `tools/host/` | USB SDP / SWD прошивка при разработке (`sdphost`/`blhost`/`nxpimage`/`pyOCD`). [README](tools/host/README.md) | +| HIL-тесты | `tools/hil/` | pytest-окружение аппаратных тестов (pyOCD + M5StampPLC). [README](tools/hil/README.md) | --- @@ -65,7 +77,8 @@ just host::debug-server # GDB-сервер для отладки | 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` | +| spsdk (nxpimage, blhost, sdphost — dev-CLI) | `tools/host/uv.lock` | +| spsdk (McuBoot/SDP/HabImage — прямой Python API), Textual | `tools/production/uv.lock` | Всё что не меняется — vendored. Сборка работает после `git clone` без интернета (кроме Python-зависимостей). diff --git a/bsp/can/README.md b/bsp/can/README.md index d6e851e..342f9af 100644 --- a/bsp/can/README.md +++ b/bsp/can/README.md @@ -65,7 +65,7 @@ bsp_status_t bsp_can_set_filter(uint8_t idx, uint32_t id, uint32_t mask, bool is_extended); bsp_status_t bsp_can_accept_all(void); -bsp_status_t bsp_can_register_rx_callback(bsp_can_rx_cb_t cb, void *p_ctx); +bsp_status_t bsp_can_register_rx_callback(bsp_can_rx_callback_t cb, void *p_ctx); ``` **Коды возврата `bsp_can_send()`:** @@ -147,7 +147,7 @@ bsp_can_set_filter(2, 0x1ABCDEF0, 0x1FFFFFFF, true); add_host_test( NAME test_bsp_can SOURCES can/test_bsp_can.c - ${PROJECT_SOURCE_DIR}/bsp/can/src/bsp_can.c + ${PROJECT_SOURCE_DIR}/bsp/can/src/can.c ${PROJECT_SOURCE_DIR}/utils/ring_buffer/ring_buffer.c INCLUDES ${PROJECT_SOURCE_DIR}/bsp/can/include @@ -160,7 +160,7 @@ add_host_test( **Humble Object** — fff-заглушки для потребителей в `bsp/can/mocks/`: ```c -#include "can_mock.h" +#include "can_mocks.h" void setUp(void) { CAN_MOCK_RESET_ALL(); } diff --git a/bsp/mqs/include/bsp/mqs.h b/bsp/mqs/include/bsp/mqs.h index 09a6240..76dd50c 100644 --- a/bsp/mqs/include/bsp/mqs.h +++ b/bsp/mqs/include/bsp/mqs.h @@ -1,10 +1,10 @@ /** * @file bsp/mqs.h - * @brief BSP: Medium Quality Sound (MQS) — SAI1 + eDMA + MQS. + * @brief BSP: Medium Quality Sound (MQS) — SAI3 + eDMA + MQS. * - * Слой абстракции над SAI1/eDMA/MQS для монофонического аудио-выхода. + * Слой абстракции над SAI3/eDMA/MQS для монофонического аудио-выхода. * Физически на плате выведен один канал (MQS_RIGHT, GPIO_AD_B0_04); - * SAI1 требует стерео-буфер — оба канала всегда идентичны. + * SAI3 требует стерео-буфер — оба канала всегда идентичны. * * Режимы использования: * - firmware_test: bsp_mqs_play_blocking() — синхронная подача @@ -38,14 +38,14 @@ extern "C" * Параметры аудио-потока * ----------------------------------------------------------------------- */ -/** Частота дискретизации, Гц. Небольшое отклонение (~0.5%) из-за - * источника SAI1_CLK_ROOT (System PLL PFD2, не Audio PLL). */ +/** Частота дискретизации, Гц. Источник — SAI3_CLK_ROOT (Audio PLL / 8 / 8), + * делитель MCLK подобран точно (8), отклонения нет. */ #define BSP_MQS_SAMPLE_RATE_HZ (44100U) /** Разрядность PCM. MQS поддерживает только 16 бит. */ #define BSP_MQS_BIT_WIDTH (16U) -/** Количество каналов в буфере. SAI1+MQS требует стерео; правый == левый. */ +/** Количество каналов в буфере. SAI3+MQS требует стерео; правый == левый. */ #define BSP_MQS_CHANNELS (2U) /** Байт на один моно-сэмпл (16 бит → 2 байта). */ @@ -73,11 +73,11 @@ extern "C" * ----------------------------------------------------------------------- */ /** - * @brief Инициализация MQS-подсистемы: SAI1, eDMA, DMAMUX, MQS. + * @brief Инициализация MQS-подсистемы: SAI3, eDMA, DMAMUX, MQS. * - * Включает тактирование SAI1 (kCLOCK_Sai1), настраивает SAI1 в режиме + * Включает тактирование SAI3 (kCLOCK_Sai3), настраивает SAI3 в режиме * TX Master, 16 бит, стерео, 44100 Гц, инициализирует eDMA канал 0 - * (DMAMUX source kDmaRequestMuxSai1Tx) и MQS-модуль. + * (DMAMUX source kDmaRequestMuxSai3Tx) и MQS-модуль. * * Пин GPIO_AD_B0_04 (MQS_RIGHT) уже замультиплексирован в BOARD_InitPins(). * MQS oversample (×32) уже выставлен в BOARD_BootClockRUN(). @@ -88,7 +88,7 @@ extern "C" bsp_status_t bsp_mqs_init(void); /** - * @brief Деинициализация: остановить DMA, сбросить SAI1 и MQS. + * @brief Деинициализация: остановить DMA, сбросить SAI3 и MQS. * * Безопасно вызывать даже если воспроизведение уже завершилось. * После вызова модуль требует повторного bsp_mqs_init(). @@ -152,7 +152,7 @@ extern "C" * ----------------------------------------------------------------------- */ /** - * @brief Инициализация усилителя: PWM4 SM0, 16 кГц, duty 50%. + * @brief Инициализация усилителя: PWM4 SM0, 12 кГц, duty 50%. * * Настраивает XBARA1 (fault disable), PWM4 submodule 0 channel A. * Вызывать до bsp_mqs_play() — без ШИМ на VOLUME усиление равно нулю. diff --git a/bsp/mqs/src/mqs.c b/bsp/mqs/src/mqs.c index ac539d3..f6ddf77 100644 --- a/bsp/mqs/src/mqs.c +++ b/bsp/mqs/src/mqs.c @@ -1,19 +1,21 @@ /** * @file bsp_mqs.c - * @brief BSP MQS: SAI1 TX + eDMA + MQS для MIMXRT1052CVJ5B. + * @brief BSP MQS: SAI3 TX + eDMA + MQS для MIMXRT1052CVJ5B. * * Тактирование: - * SAI1_CLK_ROOT = SysPLL × (18/27) / (SAI1_CLK_PRED+1=4) / (SAI1_CLK_PODF+1=2) - * ≈ 63 529 411 Гц (BOARD_BOOTCLOCKRUN_SAI1_CLK_ROOT) + * Audio PLL = 24 МГц × (30 + 66/625) = 722.534 МГц + * SAI3_CLK_ROOT = Audio PLL / 8 / 8 = 11 289 600 Гц + * (kCLOCK_Sai3Mux=2, Sai3PreDiv=7, Sai3Div=7, + * BOARD_BOOTCLOCKRUN_SAI3_CLK_ROOT) * Bit clock = 44100 × 16 × 2 = 1 411 200 Гц - * MCLK делитель = 63 529 411 / 1 411 200 ≈ 45.0 (погрешность ~0.5 %) + * MCLK делитель = 11 289 600 / 1 411 200 = 8 (точно, без погрешности) * * MQS oversample = 32, уже выставлен в BOARD_BootClockRUN() через * IOMUXC_MQSConfig(IOMUXC_GPR, kIOMUXC_MqsPwmOverSampleRate32, 0). * * Пин: GPIO_AD_B0_04 → MQS_RIGHT — замультиплексирован в BOARD_InitPins(). * - * eDMA: DMA0 канал 0, DMAMUX source kDmaRequestMuxSai1Tx. + * eDMA: DMA0 канал 0, DMAMUX source kDmaRequestMuxSai3Tx. * Канал 0 зарезервирован за bsp_mqs. Прочие модули — каналы 1+. * * SAI API (SDK 2.4.7 / fsl_sai.h, fsl_sai_edma.h 2.7.3): @@ -47,17 +49,17 @@ #define MQS_SAI_CLOCK_GATE kCLOCK_Sai3 #define MQS_SAI_CLK_FREQ BOARD_BOOTCLOCKRUN_SAI3_CLK_ROOT -/** eDMA канал, выделенный под SAI1 TX. */ +/** eDMA канал, выделенный под SAI3 TX. */ #define MQS_DMA_CHANNEL (0U) -/** DMAMUX запрос для SAI1 TX. */ +/** DMAMUX запрос для SAI3 TX. */ #define MQS_DMAMUX_SOURCE kDmaRequestMuxSai3Tx /** Приоритет прерывания DMA (ниже USB = 3, выше нормальных задач). */ #define MQS_DMA_IRQ_PRIORITY (5U) #define MQS_HMCLK_GATE kCLOCK_Mqs /** - * FIFO watermark — половина глубины FIFO SAI1. + * FIFO watermark — половина глубины FIFO SAI3. * FSL_FEATURE_SAI_FIFO_COUNTn(x) принимает экземпляр SAI и возвращает * глубину FIFO в словах (32 для RT1052). Деление на 2 даёт оптимальную * латентность DMA: запрос формируется когда в FIFO остаётся место для @@ -121,7 +123,7 @@ bsp_status_t bsp_mqs_init(void) return BSP_OK; } - /* --- Тактирование SAI1 --- */ + /* --- Тактирование SAI3 --- */ CLOCK_EnableClock(MQS_SAI_CLOCK_GATE); /* --- Тактирование MQS (CCGR0[CG2]) --- */ @@ -132,10 +134,10 @@ bsp_status_t bsp_mqs_init(void) IOMUXC_MQSEnterSoftwareReset(IOMUXC_GPR, false); IOMUXC_MQSEnable(IOMUXC_GPR, true); - /* --- SAI1: базовая инициализация (снимает reset, включает clock gate) --- */ + /* --- SAI3: базовая инициализация (снимает reset, включает clock gate) --- */ SAI_Init(MQS_SAI_BASE); - /* --- SAI1 TX: классический I2S, master, 16 бит, стерео, канал 0. --- */ + /* --- SAI3 TX: классический I2S, master, 16 бит, стерео, канал 0. --- */ sai_transceiver_t sai_cfg; SAI_GetLeftJustifiedConfig(&sai_cfg, kSAI_WordWidth16bits, kSAI_Stereo, @@ -159,7 +161,7 @@ bsp_status_t bsp_mqs_init(void) EDMA_Init(DMA0, &dma_cfg); EDMA_CreateHandle(&s_dma_handle, DMA0, MQS_DMA_CHANNEL); - /* --- DMAMUX: канал 0 → SAI1 TX --- */ + /* --- DMAMUX: канал 0 → SAI3 TX --- */ DMAMUX_Init(DMAMUX); DMAMUX_SetSource(DMAMUX, MQS_DMA_CHANNEL, (uint8_t) MQS_DMAMUX_SOURCE); DMAMUX_EnableChannel(DMAMUX, MQS_DMA_CHANNEL); @@ -188,7 +190,7 @@ void bsp_mqs_deinit(void) } SAI_TransferTerminateSendEDMA(MQS_SAI_BASE, &s_sai_tx_handle); - /* SAI_TxReset() — сброс logic + FIFO (аналог kSAI_ResetAll). + /* SAI_TxReset() — сброс logic + FIFO (аналог kSAI_ResetAll) для SAI3. * SAI_TxSoftwareReset() отсутствует в данной версии SDK. */ SAI_TxReset(MQS_SAI_BASE); IOMUXC_MQSEnable(IOMUXC_GPR, false); diff --git a/bsp/mqs/src/mqs_amp.c b/bsp/mqs/src/mqs_amp.c index b3fd481..0671b03 100644 --- a/bsp/mqs/src/mqs_amp.c +++ b/bsp/mqs/src/mqs_amp.c @@ -9,7 +9,7 @@ * аудио-сигнала на входе (MQS_RIGHT через RC-фильтр → SOUND_OUT). * * Управление громкостью: - * PWM4 SM0 PWM_A, частота 16 кГц, центрально-симметричный режим. + * PWM4 SM0 PWM_A, частота 12 кГц, центрально-симметричный режим. * duty 0% → DC_VOL ≈ 0 В → усиление минимально (тишина). * duty 50% → DC_VOL ≈ 2.5 В → номинальная громкость. * duty 100%→ DC_VOL ≈ 5 В → максимальное усиление. @@ -22,7 +22,7 @@ * Тактирование: * IPG clock = AHB/4 = 600/4 = 150 МГц. * PWM prescaler = /16 → PWM clock = 9.375 МГц. - * Fpwm = 16000 Гц (центрально-симметричный режим). + * Fpwm = 9 375 000 / 586 / 2 = 12000 Гц (центрально-симметричный режим). */ #include "bsp/mqs.h" @@ -96,7 +96,7 @@ bsp_status_t bsp_mqs_amp_init(void) /* --- ForceSignal: использовать нормальный PWM-сигнал --- */ PWM_SetupForceSignal(AMP_PWM_BASE, AMP_PWM_SUBMODULE, AMP_PWM_CHANNEL, kPWM_UsePwm); - /* --- PWM-сигнал: 16 кГц, центрально-симметричный, duty 50% --- */ + /* --- PWM-сигнал: 12 кГц, центрально-симметричный, duty 50% --- */ const pwm_signal_param_t PWM_SIGNAL = { .pwmChannel = AMP_PWM_CHANNEL, .dutyCyclePercent = AMP_DEFAULT_DUTY, diff --git a/bsp/provisioning/include/bsp/provisioning.h b/bsp/provisioning/include/bsp/provisioning.h index 4607b9a..b3eea7d 100644 --- a/bsp/provisioning/include/bsp/provisioning.h +++ b/bsp/provisioning/include/bsp/provisioning.h @@ -25,9 +25,8 @@ * @brief Прочитать уникальный идентификатор чипа из OCOTP. * * Читает OCOTP_CFG0 (UID[31:0]) и OCOTP_CFG1 (UID[63:32]). - * Результат записывается в big-endian порядке: p_uid[0] — старший байт CFG1, - * p_uid[7] — младший байт CFG0. Hex-строка совпадает с визуальным порядком слов - * в Reference Manual (MIMXRT1052RM Table 46-2). + * Результат записывается в нативном порядке байт (little-endian на Cortex-M7): + * p_uid[0..3] = CFG0 (UID[31:0]), p_uid[4..7] = CFG1 (UID[63:32]). * * Функция выполняет OCOTP_Init() и включает clock gate перед чтением. * Clock gate остаётся открытым после вызова (паттерн проекта). diff --git a/bsp/uart_host/README.md b/bsp/uart_host/README.md index 5ce90d6..dde9926 100644 --- a/bsp/uart_host/README.md +++ b/bsp/uart_host/README.md @@ -44,12 +44,16 @@ flowchart TD ```c bsp_status_t bsp_uart_host_init(uint32_t baud); +void bsp_uart_host_deinit(void); bsp_status_t bsp_uart_host_write(const uint8_t *p_data, size_t len); bsp_status_t bsp_uart_host_write_str(const char *p_str); size_t bsp_uart_host_read(uint8_t *p_buf, size_t len, uint32_t timeout_ms); int32_t bsp_uart_host_read_byte(uint32_t timeout_ms); + +size_t bsp_uart_host_rx_available(void); /* байт в RX-буфере прямо сейчас */ +void bsp_uart_host_rx_flush(void); /* сбросить содержимое RX-буфера */ ``` `bsp_uart_host_read()` возвращает фактически прочитанное количество байт — diff --git a/bsp/usb_cdc/README.md b/bsp/usb_cdc/README.md index b07bd32..7697c57 100644 --- a/bsp/usb_cdc/README.md +++ b/bsp/usb_cdc/README.md @@ -19,7 +19,9 @@ COM-порт (`/dev/ttyACM*` на Linux/macOS, `COMx` на Windows). Испол Скорость: High-Speed (480 Mbit/s), fallback Full-Speed (12 Mbit/s). PHY калибровка: `D_CAL=0x0C`, `TXCAL45DP=0x06`, `TXCAL45DM=0x06`. -**VID/PID**: `0x1234` / `0x0001` — placeholder, заменить на производственные. +**VID/PID**: `0x1996` / `0x00AD` (`usb_device_descriptor.h`) — тот же +идентификатор, что `tools/production/` (service-tui) использует для +детекта CDC-порта firmware_test (`SERVICE_CDC_VID`/`SERVICE_CDC_PID`). --- diff --git a/docs/DEV_ARCH.md b/docs/DEV_ARCH.md index 61bd5f8..dc822f5 100644 --- a/docs/DEV_ARCH.md +++ b/docs/DEV_ARCH.md @@ -210,6 +210,7 @@ flowchart LR │ ├── 01_test_uart.py ← HIL тест bsp_uart_host (без M5) │ ├── 02_test_opto.py ← HIL тест bsp_opto (через M5StampPLC) │ ├── 03_test_can.py ← HIL тест bsp_can +│ ├── 04_test_button.py ← HIL тест bsp_button (интерактивный, оператор) │ ├── 05_test_usb_cdc.py ← HIL тест USB CDC (bsp_usb_cdc, UART CLI) │ ├── 06_test_firmware_opto.py ← HIL тест opto через firmware_test CDC │ ├── 06_test_firmware_can.py ← HIL тест CAN через firmware_test CDC diff --git a/docs/HOW_TO_FLASH.md b/docs/HOW_TO_FLASH.md index d47c9cb..33546f4 100644 --- a/docs/HOW_TO_FLASH.md +++ b/docs/HOW_TO_FLASH.md @@ -81,16 +81,21 @@ auto-config не подтверждена — см. 1.5. `service-tui` (`tools/production/`) умеет прошивать бинарники, собранные не в этом репозитории (например, старые платы с W25Q512), тем же способом -(USB SDP), но с двумя отличиями от штатного пути: +(USB SDP), но с двумя отличиями от штатного пути. Это **отдельная +реализация**, не связанная с `flash_usb.py`/`nxpimage` CLI — TUI прошивает +in-process через Python API `spsdk` (`app/flash_backend.py`: `HabImage`, +`McuBoot`, `SDP`), без единого subprocess: - HAB-образ (IVT + опционально DCD) собирается из **сырого** бинарника на - лету через `nxpimage`, а не заранее через `just build::hab-*` -- FCB пишется **явно** (`flash_usb.py --fcb-path tools/host/dcd/w25qXXX_fdcb.bin`, - буквальный `write-memory` вместо `configure-memory 0xF000000F`) — auto-config - для 4-байтной адресации не проверялся, решили на него не полагаться + лету через `HabImage` (spsdk), а не заранее через `just build::hab-*` +- FCB пишется **явно** (`mboot.write_memory()` с готовым блобом + `tools/host/dcd/w25qXXX_fdcb.bin`, буквальная запись вместо + `configure-memory 0xF000000F`) — auto-config для 4-байтной адресации не + проверялся, решили на него не полагаться -Подробности конвейера — в `tools/production/DEV_ARCH.md`, §8. Штатный путь -(`--firmware`, три сборки этого репозитория) не меняется и по-прежнему +Подробности конвейера — в [tools/production/docs/DEV_ARCH.md](../tools/production/docs/DEV_ARCH.md), +§8. Штатный путь (`--firmware`, три сборки этого репозитория, что через +`just host::flash`, что через `service-tui`) не меняется и по-прежнему использует auto-config Flashloader, как описано в 1.4. --- diff --git a/docs/testing/PROTOCOL.md b/docs/testing/PROTOCOL.md index 579c2b5..7a5ea15 100644 --- a/docs/testing/PROTOCOL.md +++ b/docs/testing/PROTOCOL.md @@ -4,7 +4,13 @@ > > Документ описывает протокол обмена между диагностической прошивкой > (`firmware_test`) и хостовым ПО сервисного инженера. -> Актуален для: `firmware_test v0.1.0+`, `protocol.h v2`. +> Актуален для: `firmware_test v0.1.2+`, `protocol.h v2`. +> +> Полный справочник по каждому тесту (потоки, коды `detail`, таблица HIL +> реле) — в [firmware/test/README.md](../../firmware/test/README.md) и +> [firmware/test/src/tests/README.md](../../firmware/test/src/tests/README.md). +> Этот документ — сжатый протокольный обзор с точки зрения хостового ПО +> (TUI/pytest), а не полное описание тест-логики. --- @@ -79,7 +85,7 @@ sequenceDiagram participant T as Таргет Note over T: прошивка загружена через USB SDP - T-->>H: {"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0} + T-->>H: {"type":"session_start","fw":"0.1.2","target":"IMXRT1052","uptime_ms":0} H->>T: {"type":"cmd","cmd":"ping"} T-->>H: {"type":"pong"} @@ -169,6 +175,17 @@ sequenceDiagram ← {"ok":false,"error":"UID_READ_ERR"} ``` +### `get_version` — чтение версии прошивки + +```json +→ {"type":"cmd","cmd":"get_version"} +← {"type":"version_response","fw":"0.1.2"} +``` + +Дублирует значение `"fw"` из `session_start` — полезно, если хост +подключился уже после того, как `session_start` был отправлен (может быть +пропущен, это одноразовое событие сразу после старта). + ### `run_selected` — запуск подмножества тестов Запускает тесты по списку ID. Порядок выполнения — по реестру таргета, @@ -199,7 +216,7 @@ sequenceDiagram ### `session_start` ```json -{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0} +{"type":"session_start","fw":"0.1.2","target":"IMXRT1052","uptime_ms":0} ``` ### `test_begin` @@ -222,6 +239,11 @@ sequenceDiagram `detail` — ASCII-строка до 95 символов. При `pass` — пустая. +### `test_list` + +Ответ на `list_tests` — массив дескрипторов теста (`id`, `name`, +`critical`, `requires_hil`), см. пример в разделе `list_tests` выше. + ### `progress` ```json @@ -230,6 +252,11 @@ sequenceDiagram Промежуточные шаги внутри теста. Используется в `usd`. +### `uid_response` / `version_response` + +Ответы на `get_uid`/`get_version` — см. описание соответствующих команд +выше. + ### `confirm_request` ```json @@ -270,25 +297,30 @@ sequenceDiagram | `UNKNOWN_TEST` | Поле `"id"` в `run` или `"tests"` в `run_selected` содержит неизвестный ID | | `LINE_TOO_LONG` | Входящая строка превысила 128 байт | | `BUSY` | Таргет выполняет тест, новая команда отклонена | +| `UID_READ_ERR` | `bsp_prov_read_uid()` вернул ошибку (ответ на `get_uid`) | --- ## Матрица тестов -| ID | Название | Тип | Critical | HIL (M5) | Интерактивный | -| ---------- | --------------------- | ------------------ | -------- | -------- | ------------------ | -| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ | -| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ | -| `usd` | uSD (SDIO) | self + interactive | ❌ | ❌ | ✅ (вставить карту) | -| `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ (цвета R/G/B/W) | -| `buttons` | Кнопки Test_But_1/2 | interactive | ❌ | ❌ | ✅ (нажать кнопки) | -| `can` | CAN | HIL | ❌ | ✅ | ❌ | -| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ | -| `uart_iso` | UART ISO / RS_RX Opto | HIL | ❌ | ✅ | ❌ | -| `opto` | Opto-in EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ | +Порядок — как в реестре `k_registry[]` (`test_runner.c`); полная версия с +кодами `detail` и HIL-таблицей реле — в +[firmware/test/README.md §Матрица тестов](../../firmware/test/README.md#матрица-тестов). + +| ID | Название | Тип | Critical | HIL (M5) | Интерактивный | +| --------- | ------------------- | ------------------ | -------- | -------- | --------------------- | +| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ | +| `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ | +| `usd` | microSD (SDIO) | self + interactive | ❌ | ❌ | ✅ (вставить карту) | +| `display` | TFT Display RGB888 | interactive | ❌ | ❌ | ✅ (6 шагов, см. ниже) | +| `buttons` | Test Buttons | interactive | ❌ | ❌ | ✅ (нажать кнопки) | +| `opto` | Opto Inputs | HIL | ❌ | ✅ | ❌ (авто, 6 шагов) | +| `can` | CAN loopback | HIL | ❌ | ✅ | ❌ (авто, 2 шага) | +| `mqs` | MQS Audio Out | interactive | ❌ | ❌ | ✅ (слышимость тона) | **Типы:** **self** — таргет тестирует периферию самостоятельно; **interactive** — -требует `confirm_request`; **HIL** — требует M5StampPLC. +требует `confirm_request`, отвечает оператор; **HIL** — требует M5StampPLC, +confirm автоматический (без оператора). --- @@ -320,6 +352,10 @@ sequenceDiagram ### Display (RGB888) +Шесть шагов: Red → Green → Blue → White, затем два ротационных (диагностика +непропаянных LR/UD пинов на TFT7/8/10). Тест прерывается на **первом** +неподтверждённом шаге. + ```mermaid sequenceDiagram participant H as Хост @@ -333,10 +369,36 @@ sequenceDiagram T-->>H: {"type":"confirm_request","id":"display_blue","prompt":"Экран залит синим?","timeout_ms":15000} H->>T: {"type":"confirm","id":"display_blue","confirmed":true} T-->>H: {"type":"confirm_request","id":"display_white","prompt":"Экран залит белым?","timeout_ms":15000} - H->>T: {"type":"confirm","id":"display_white","confirmed":false} - T-->>H: {"type":"test_result","id":"display","status":"fail","ms":22103,"detail":"display_white not confirmed"} + H->>T: {"type":"confirm","id":"display_white","confirmed":true} + T-->>H: {"type":"confirm_request","id":"display_rot0","prompt":"Слева КРАСНЫЙ, справа СИНИЙ?","timeout_ms":15000} + H->>T: {"type":"confirm","id":"display_rot0","confirmed":true} + T-->>H: {"type":"confirm_request","id":"display_rot_base","prompt":"Красный/синий поменялись сторонами?","timeout_ms":15000} + H->>T: {"type":"confirm","id":"display_rot_base","confirmed":true} + T-->>H: {"type":"test_result","id":"display","status":"pass","ms":42310,"detail":""} ``` +При отказе/таймауте на любом шаге: `status:"fail"`, +`detail:" not confirmed"` (например, `"display_white not confirmed"`). + +### MQS Audio Out + +Таргет ~4с играет мелодию через MQS + усилитель, затем запрашивает +подтверждение слышимости — единственный тест с аудио-confirm: + +```mermaid +sequenceDiagram + participant H as Хост + participant T as Таргет + + T-->>H: {"type":"test_begin","id":"mqs",...} + Note over T: ~4с воспроизведение тона (A4, затем E5) + T-->>H: {"type":"confirm_request","id":"mqs_tone","prompt":"Do you hear a tone?","timeout_ms":15000} + H->>T: {"type":"confirm","id":"mqs_tone","confirmed":true} + T-->>H: {"type":"test_result","id":"mqs","status":"pass","ms":19240,"detail":""} +``` + +Отказ/таймаут → `status:"fail"`, `detail:"operator: no sound"`. + ### Кнопки ```mermaid @@ -393,32 +455,15 @@ firmware/test/src/ ├── test_usd.c ├── test_display.c ├── test_buttons.c + ├── test_opto.c ├── test_can.c - ├── test_uart_ttl.c - ├── test_uart_iso.c - └── test_opto.c + └── test_mqs.c ``` ### Добавление нового теста -1. Создать `firmware/test/src/tests/test_foo.c`. -2. Объявить дескриптор: - -```c -const test_module_t k_test_foo = { - .id = "foo", - .name = "Foo Peripheral", - .critical = false, - .requires_hil = false, - .pre_confirm_prompt = NULL, - .init = NULL, - .run = test_foo_run, - .deinit = NULL, -}; -``` - -1. Добавить `&k_test_foo` в реестр `test_runner.c`. -2. Добавить `tests/test_foo.c` в `CMakeLists.txt` таргета. +Пошаговый гайд с шаблонами (self-тест, интерактивный, pre-confirm) — +[firmware/test/README.md §Как добавить новый тест](../../firmware/test/README.md#как-добавить-новый-тест). --- diff --git a/docs/testing/hil/HIL_CREATE_TEST.md b/docs/testing/hil/HIL_CREATE_TEST.md index 8139258..5272bcb 100644 --- a/docs/testing/hil/HIL_CREATE_TEST.md +++ b/docs/testing/hil/HIL_CREATE_TEST.md @@ -166,12 +166,18 @@ ls build/target-debug/tests/target//test_.elf ## Шаг 5 — `conftest.py`: добавить фикстуры -### Базовый тест (без M5) +### Любой тест — фикстура загрузки всегда зависит от `m5` + +M5StampPLC управляет питанием таргета (RLY1 → VIN, см. `HIL_BENCH.md`), а +не только сигнальными реле — поэтому `loaded_` зависит от `m5` **во всех +случаях**, даже если сам тест не использует реле для сигналов (например, +`01_test_uart.py`/`loaded_host_uart`). Без этой зависимости pyOCD попытается +подключиться к обесточенной плате. ```python -# 1. Фикстура загрузки +# 1. Фикстура загрузки — m5 гарантирует, что питание включено до pyOCD @pytest.fixture(scope="module") -def loaded_(request: pytest.FixtureRequest) -> None: +def loaded_(request: pytest.FixtureRequest, m5: M5Agent) -> None: _load_elf( request, Path(cfg.BUILD_DIR) / "tests/target//test_.elf", @@ -184,26 +190,10 @@ _UART_FIXTURE_MAP = { } ``` -### Тест с M5 - -```python -# 1. Зависимость от m5 гарантирует что питание включено до загрузки ELF -@pytest.fixture(scope="module") -def loaded_(request: pytest.FixtureRequest, m5: M5Agent) -> None: - _load_elf( - request, - Path(cfg.BUILD_DIR) / "tests/target//test_.elf", - ) - -# 2. UART-фикстура — та же одна строка -_UART_FIXTURE_MAP = { - ... - "uart_": "loaded_", -} -``` - -**Правило:** если тест управляет железом через M5 — `loaded_` должен явно -зависеть от `m5`, иначе pyOCD попытается подключиться до включения питания. +Различие между «базовым» и «с M5» тестом — не в сигнатуре `loaded_` +(она всегда одна и та же), а в том, использует ли сам **тест-кейс** +`m5.opto_set()`/`m5.relay_set()`/`m5.can_*()` для управления сигналами +помимо включения питания (см. пример «Тест с M5» в Шаге 6 ниже). --- diff --git a/docs/testing/host/HOST_CREATE_TEST.md b/docs/testing/host/HOST_CREATE_TEST.md index 1fc921d..a05b283 100644 --- a/docs/testing/host/HOST_CREATE_TEST.md +++ b/docs/testing/host/HOST_CREATE_TEST.md @@ -1,232 +1,193 @@ -# Отладка прошивок через SWD + GDB +# Добавление нового host unit-теста -## Обзор архитектуры +Пошаговый гайд для разработчика. Полный справочник по Unity/FFF API, +структуре stub-хедеров и типичным ловушкам — в +[tests/host/README.md](../../../tests/host/README.md). Этот документ — +только про шаги добавления нового теста в сборку. -Отладка построена на пробросе GDB-сервера с хоста в devcontainer по TCP. Это -позволяет держать весь инструментарий сборки и языковой сервер внутри контейнера, -не проводя USB-пробник внутрь Docker. +--- + +## Обзор стека ```mermaid flowchart LR - subgraph Host["Хост (macOS / Linux)"] - DS["just host::debug-server\npyocd gdbserver :3333"] - ML["MCU-Link (CMSIS-DAP)"] - DS --> ML + subgraph DC["Devcontainer (единственное место запуска)"] + C["tests/host/<dir>/test_<name>.c\nUnity [+ fff]"] + CP["CMakePresets.json\nhost-debug / host-release"] + JB["just/build.just\ntest-host"] + C --> CP --> JB end - - subgraph DC["Devcontainer"] - CD["cortex-debug\n(VSCode F5)"] - GDB["arm-none-eabi-gdb\nсимволы из .elf"] - CD --> GDB - end - - Board["MIMXRT1052\nFlash / SDRAM\nSEGGER RTT буфер"] - - GDB -->|"TCP host.docker.internal:3333"| DS - ML -->|"SWD"| Board ``` -**Ключевой принцип:** `pyocd gdbserver` слушает на `0.0.0.0:3333`. Из контейнера -GDB подключается через `host.docker.internal:3333` — специальный DNS-алиас Docker, -резолвится в IP хост-машины. +Host-тесты компилируются `clang-17` **на хосте** (не ARM GCC), исполняются +как обычные нативные бинарники под `ctest`. Никакого железа не требуется — +в отличие от HIL-тестов (см. [../hil/HIL_CREATE_TEST.md](../hil/HIL_CREATE_TEST.md)). --- -## Компоненты +## Шаг 0 — Определить категорию модуля -### На хосте +| Категория | Инструментарий | Пример | +| ----------------------------------------- | ------------------ | ----------------------------------- | +| **A** — платформонезависимый | Только Unity | `protocol.c`, `test_runner.c`, `ring_buffer.c` | +| **B** — BSP-модуль (зависит от NXP SDK) | Unity + fff + stub-хедеры | `bsp/led`, `bsp/opto`, `bsp/can`, `bsp/button` | -| Компонент | Роль | Источник | -| --------------------------------- | ------------------------------- | -------------------------- | -| `pyocd` | GDB-сервер + flash-программатор | `tools/hil/uv.lock` | -| `MCU-Link` | CMSIS-DAP v2 пробник | USB к плате | -| `just host::debug-server` | Запуск GDB-сервера | `just/host.just` | -| `just host::flash-swd-*` | Прошивка через SWD | `just/host.just` | -| `tools/host/flash_swd.py` | Сборка FCB+HAB образа и запись | `tools/host/` | -| `tools/host/dcd/w25q128_fdcb.bin` | FCB для W25Q128 (Quad SPI) | NXP SecureProvisioningTool | - -### В devcontainer - -| Компонент | Роль | -| -------------------------------------- | ---------------------------------------------- | -| `arm-none-eabi-gdb` | GDB клиент, подключается к серверу на хосте | -| `cortex-debug` (VSCode extension) | UI для GDB: брейкпоинты, стек, регистры | -| `.vscode/launch.json` | Конфигурации запуска отладки | -| `.vscode/tasks.json` | `preLaunchTask` — пересборка ELF перед стартом | -| `build/Debug/*.elf` | Символы для GDB (DWARF debug info) | -| `bsp/generated/startup/MIMXRT1052.xml` | SVD — описание регистров периферии | - -### Конфигурация - -Параметры отладки задаются в `.env`: - -```bash -GDB_PORT=3333 -PYOCD_TARGET=mimxrt1050_quadspi -PYOCD_FREQUENCY=4000000 -FCB_PATH=tools/host/dcd/w25q128_fdcb.bin -``` +Полное объяснение разницы и структуры — в +[tests/host/README.md §1](../../../tests/host/README.md#1-две-категории-тестируемых-модулей). --- -## Поддерживаемые прошивки - -| Конфигурация VSCode | ELF | Особенности | -| ----------------------------- | ------------------------------- | ---------------------------- | -| `🐛 Debug: firmware_test` | `build/Debug/firmware_test.elf` | Bare-metal, входной контроль | -| `🐛 Debug: bootloader` | `build/Debug/bootloader.elf` | Bare-metal, A/B обновление | -| `🐛 Debug: tft_app (FreeRTOS)` | `build/Debug/app.elf` | FreeRTOS, task view | - -Все три — XIP-прошивки, исполняются из QuadSPI NOR Flash (`0x60000000`). - ---- - -## Режимы запуска отладки - -### Режим А — прошивка уже в Flash +## Шаг 1 — Создать тестовый файл ```bash -# 1. Хост — запустить GDB-сервер (оставить в отдельном терминале) -just host::debug-server - -# 2. DevContainer — VSCode -# Run & Debug (Ctrl+Shift+D) → выбрать конфигурацию → F5 +mkdir -p tests/host// +touch tests/host//test_.c ``` -GDB сбрасывает MCU, загружает символы из ELF и останавливается на входе -в `main`. Flash не перезаписывается. - -### Режим Б — прошить через SWD, затем отладить - -```bash -# 1. DevContainer -just build::hab-firmware-test-debug - -# 2. Хост -just host::flash-swd-test-debug - -# 3. ⚡ Power cycle платы (обязательно) - -# 4. Хост -just host::debug-server - -# 5. DevContainer — VSCode → 🐛 Debug: firmware_test → F5 -``` - -### Режим В — прошить через USB SDP, затем отладить - -```bash -# 1. DevContainer -just build::build-firmware-test-debug - -# 2. Хост — перевести плату в SDP-режим, затем: -just host::flash-test-debug - -# 3. Хост -just host::debug-server - -# 4. DevContainer — VSCode → 🐛 Debug: firmware_test → F5 -``` - ---- - -## Почему flash через SWD требует FCB - -При USB SDP ROM-загрузчик инициализирует FlexSPI по DCD из HAB-образа — FCB -не нужен. При SWD flash-алгоритм pyOCD пишет в NOR Flash напрямую. При -cold-start Boot ROM сначала читает FCB по адресу `0x60000000`, конфигурирует -FlexSPI, и только потом ищет IVT. Без FCB бутлоадер не стартует. - -`flash_swd.py` решает это, собирая образ перед записью: - -```bash -0x60000000 w25q128_fdcb.bin (512 байт) — FCB -0x60000200 0xFF × 3584 байт — padding -0x60001000 firmware_test_hab.bin — IVT + DCD + код -``` - -Весь диапазон `0x60000000–0x6000FFFF` — один 64KB сектор: стирается и -записывается за одну транзакцию. - ---- - -## RTT-логи - -SEGGER RTT включён только в Debug-сборках (`SEGGER_RTT_ENABLED=ON`). -После старта отладки вкладка `TERMINAL → RTT` принимает вывод канала 0. -`cortex-debug` находит адрес буфера по символу `_SEGGER_RTT` из ELF. +### Шаблон — категория A (без моков) ```c -#include "SEGGER_RTT.h" -SEGGER_RTT_printf(0, "value = %d\n", value); +#include "unity.h" +#include "<модуль>.h" /* тестируемый модуль */ + +void setUp(void) { /* сброс состояния если нужен */ } +void tearDown(void) { } + +void test_something(void) +{ + TEST_ASSERT_EQUAL(expected, actual); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_something); + return UNITY_END(); +} +``` + +### Шаблон — категория B (с fff-фейками) + +```c +#include "unity.h" +#include "fff.h" + +DEFINE_FFF_GLOBALS; /* ровно один раз на файл */ + +/* 1. Stub-хедер с типами NXP SDK */ +#include "fsl_gpio.h" + +/* 2. Фейки для функций, которые вызывает тестируемый модуль */ +FAKE_VOID_FUNC(GPIO_PinInit, GPIO_Type *, uint32_t, const gpio_pin_config_t *); +FAKE_VOID_FUNC(GPIO_PinWrite, GPIO_Type *, uint32_t, uint8_t); + +/* 3. Тестируемый модуль — ПОСЛЕ фейков */ +#include "bsp/.h" + +void setUp(void) +{ + RESET_FAKE(GPIO_PinInit); + RESET_FAKE(GPIO_PinWrite); + FFF_RESET_HISTORY(); +} + +void tearDown(void) { } + +void test_something(void) +{ + TEST_ASSERT_EQUAL_UINT8(0U, GPIO_PinWrite_fake.arg2_val); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_something); + return UNITY_END(); +} +``` + +Если тестируемому модулю не хватает stub-хедера (новый SDK-вызов) — +добавить минимальные типы/сигнатуры в `tests/host/mocks/` (только то, что +реально используется — не копировать весь SDK-хедер). + +--- + +## Шаг 2 — Зарегистрировать в `tests/host/CMakeLists.txt` + +```cmake +# категория A — платформонезависимый, без MOCKS +add_host_test( + NAME test_ + SOURCES /test_.c + ${PROJECT_SOURCE_DIR}/<путь-к-модулю>/.c + INCLUDES ${PROJECT_SOURCE_DIR}/<путь-к-инклюдам> +) + +# категория B — BSP-модуль, нужны MOCKS +add_host_test( + NAME test_ + SOURCES /test_.c + ${PROJECT_SOURCE_DIR}/bsp//src/.c + INCLUDES ${PROJECT_SOURCE_DIR}/bsp//include + ${PROJECT_SOURCE_DIR}/bsp/common/include + MOCKS ${BSP_MOCKS_DIR} +) +``` + +`add_host_test()` — вспомогательная CMake-функция, определённая в начале +того же файла (`NAME`/`SOURCES`/`INCLUDES`/`MOCKS`). Каждый тест — свой +исполняемый файл; `MOCKS` подключает `tests/host/mocks/` в include path +**раньше** реального SDK, `INCLUDES` — явные пути, специфичные для теста +(без скрытых глобальных путей). Если модуль использует `bsp_uart_host` через +готовый мок — смотри пример `uart_host_mock_example` в том же файле. + +Если тест компилируется с seam-макросом (как `test_runner.c` с +`-DUNIT_TEST`, см. `firmware/test/README.md` §UNIT_TEST seam) — добавить: + +```cmake +target_compile_definitions(test_ PRIVATE UNIT_TEST) ``` --- -## FreeRTOS task view - -Конфигурация `🐛 Debug: tft_app (FreeRTOS)` включает `"rtos": "FreeRTOS"` — -cortex-debug разбирает структуры планировщика и показывает вкладку `RTOS` -с таблицей задач: имя, состояние, использование стека, приоритет. - ---- - -## Просмотр регистров периферии - -Вкладка `Peripherals` показывает все блоки MIMXRT1052 по SVD-файлу -`bsp/generated/startup/MIMXRT1052.xml`. Значения обновляются при каждой паузе. - ---- - -## Ограничения - -**MCU-Link монопольный ресурс.** `debug-server` и `flash-swd` не могут -работать одновременно. Перед `flash-swd` остановите сервер (Ctrl+C). - -**HIL-тесты vs отладка.** pyOCD также используется для HIL. Перед -`just host::hil-run` остановите GDB-сервер. - -**Power cycle после flash-swd обязателен.** VECTRESET не реинициализирует -FlexSPI — только полное отключение питания гарантирует корректный cold-start. - -**Только Debug-сборки.** Release компилируется с `-O2` без DWARF-символов. - ---- - -## Быстрый старт (первый запуск) +## Шаг 3 — Собрать и прогнать ```bash -# 1. Убедиться что в .devcontainer/devcontainer.json есть (для Linux): -# "runArgs": ["--add-host=host.docker.internal:host-gateway"] +# конфигурация (один раз или после изменения CMakeLists) +cmake --preset host-debug -# 2. Залить прошивку -just host::flash-test-debug +# сборка + тесты одной командой +just build::test-host -# 3. Хост — запустить GDB-сервер -just host::debug-server +# конкретный тест с полным выводом Unity +ctest --preset host-debug-test -R test_ -V -# 4. DevContainer — VSCode -# Ctrl+Shift+D → 🐛 Debug: firmware_test → F5 +# напрямую — без обёртки CTest +./build/host-debug/tests/host/test_ +``` + +`just build::test-host` собирает под пресетом `host-debug` (`clang-17`, +без ARM-специфики) и прогоняет весь набор через CTest. `host-release` +собирает тот же набор с оптимизациями — используется в CI как +дополнительный гейт. + +--- + +## Чеклист + +```bash +[ ] tests/host//test_.c — тест-файл (категория A или B) +[ ] tests/host/mocks/*.h — новый stub-хедер, если модуль + использует ранее не замоканный SDK-вызов +[ ] tests/host/CMakeLists.txt — add_host_test(...) для нового теста +[ ] just build::test-host — зелёная сборка + прогон ``` --- -## Дерево файлов отладки +## Справочник -```bash -. -├── .env # GDB_PORT, PYOCD_TARGET, PYOCD_FREQUENCY, FCB_PATH -├── .vscode/ -│ ├── launch.json # cortex-debug конфигурации (3 проекта) -│ └── tasks.json # preLaunchTask: build:*-debug -├── bsp/generated/startup/ -│ └── MIMXRT1052.xml # SVD — регистры периферии -├── just/ -│ └── host.just # debug-server, flash-swd-* -└── tools/ - ├── hil/ # uv-проект с pyocd - └── host/ - ├── flash_swd.py # FCB + HAB → Flash через pyOCD - └── dcd/ - └── w25q128_fdcb.bin # FCB для W25Q128 Quad SPI -``` +Полный API Unity (assertion-макросы), fff (создание фейков, `custom_fake`, +проверка вызовов), работа со stub-хедерами и типичные ловушки (dangling +pointer из `arg_history`, `static`-функции, `ScopeMismatch`-аналоги для +host-тестов) — в [tests/host/README.md](../../../tests/host/README.md). diff --git a/tools/production/README.md b/tools/production/README.md index 395117a..05d04c2 100644 --- a/tools/production/README.md +++ b/tools/production/README.md @@ -18,14 +18,19 @@ TUI-приложение для диагностики и прошивки пл ```bash ┌────────────────────────────────────────────────────┐ -│ service_tool v0.3.0 │ +│ service_tool v0.2.0 │ │ │ │ [LOGO_ART] │ │ │ │ Подключите плату индикатора к USB... ⠋ │ +│ │ +│ [ ✕ Выйти из приложения ] │ └────────────────────────────────────────────────────┘ ``` +Версия читается из `pyproject.toml` — при бампе версии мокап выше не нужно +обновлять руками, TUI подставит актуальную сама. + При потере соединения на любом другом экране сессия разрывается полностью — TUI не пытается восстановить прежнее состояние, а возвращается сюда. Над подсказкой на 4 секунды появляется строка `⚠ <причина>` (например, @@ -55,7 +60,7 @@ TUI не пытается восстановить прежнее состоян │ │ │ ████████████░░░░░░ ← без числового % │ │ ┌────────────────────────────────────────────┐ │ -│ │ ▶ Сборка HAB-образа (nxpimage)... │ │ +│ │ ▶ Сборка HAB-образа (HabImage)... │ │ │ │ ▶ Прошивка: TFT_BOOTLOADER_NEW │ │ │ │ ... │ │ │ └────────────────────────────────────────────┘ │ @@ -70,10 +75,11 @@ TUI не пытается восстановить прежнее состоян **"Другое" — для бинарников, собранных не в этом репозитории.** В `custom_binaries/` кладётся **сырой** образ (код + таблица векторов, без -FCB/IVT/DCD — то же самое, что `build/Debug/bootloader.bin` до `nxpimage`). -TUI сама собирает из него загружаемый образ на лету: +FCB/IVT/DCD — то же самое, что `build/Debug/bootloader.bin` до сборки HAB). +TUI сама собирает из него загружаемый образ на лету, in-process через +Python API `spsdk` (без вызова внешних CLI-утилит): -1. `nxpimage hab export` — добавляет IVT (+DCD, если включён тумблер "Использует SDRAM") +1. `HabImage` (spsdk) — добавляет IVT (+DCD, если включён тумблер "Использует SDRAM") 2. в Flash пишется явный FCB под выбранную память платы (не тот же auto-config, что для штатных `firmware_test`/`bootloader`/`app` — для W25Q256/512 он ненадёжен, см. `DEV_ARCH.md`) @@ -193,6 +199,19 @@ Production/Custom этот шаг не нужен). --- +## Известные ограничения + +- **Одна плата на столе одновременно.** В SDP/Flashloader-режиме плату + нельзя идентифицировать по UID — авто-прошивка по факту детекта без + подтверждения оператора убрала бы последний шанс заметить, что в руках + не та плата. Массового программирования (несколько плат параллельно) + нет и не планируется в этом виде — см. `docs/DEV_ARCH.md`, §8. +- **Циклический прогон тестов** (повторный автозапуск набора без ручного + нажатия на каждую итерацию) — отложен на пост-релиз, не входит в текущую + версию. + +--- + ## Конфигурация (`.env`) ```ini @@ -223,6 +242,11 @@ FIRMWARE_BUILD_TYPE=Debug # Опционально: путь к директории лога TUI # SERVICE_LOG_DIR=/tmp + +# Уровень логирования. По умолчанию — INFO, при этом spsdk/libusbsio +# принудительно приглушены до WARNING. DEBUG включает полный дамп, +# включая сырые HID-пакеты spsdk (много строк на одну прошивку). +# SERVICE_LOG_LEVEL=DEBUG ``` --- @@ -239,38 +263,48 @@ just host::service-tui # запустить TUI ### Standalone-бинарь (сервисник) ```bash -just host::service-build -# → tools/production/dist/service_tui +just host::package-tui +# → tools/production/dist/service-tui-vX.Y.Z-/ ``` -> Standalone-бинарь не включает `tools/host/` — для прошивки рядом нужен -> инициализированный `tools/host/` (`just host::setup-tools`), либо -> абсолютный путь в `_FLASH_USB_SCRIPT` (`flasher.py`). +Бандл (PyInstaller, onedir) самодостаточен — прошивка идёт напрямую через +spsdk Python API (`app/flash_backend.py`), без вызова `tools/host/` ни +субпроцессом, ни как-либо иначе. Не нужны ни Zadig, ни установленный +Python/uv на машине сервисника. Структура бандла и резолв путей во frozen — +см. [DEV_ARCH.md](docs/DEV_ARCH.md), §14. --- ## Зависимости -| Пакет | Версия | Назначение | -| --------------- | ------ | ----------------------------------------------------- | -| `textual` | ≥ 0.80 | TUI фреймворк | -| `pyserial` | ≥ 3.5 | USB CDC ACM + M5 Serial | -| `pyusb` | ≥ 1.0 | детект BootROM SDP (не виден через pyserial на macOS) | -| `python-dotenv` | ≥ 1.0 | загрузка `.env` | -| `pyinstaller` | ≥ 6.0 | сборка standalone-бинаря | +| Пакет | Версия | Назначение | +| --------------- | ------- | -------------------------------------------------------- | +| `textual` | ≥ 0.80 | TUI фреймворк | +| `pyserial` | ≥ 3.5 | USB CDC ACM + M5 Serial | +| `pyusb` | ≥ 1.0 | не используется в коде (детект SDP/CDC идёт через `spsdk`) — исторический остаток, кандидат на удаление из `pyproject.toml` | +| `spsdk` | 3.7.0 | прошивка: SDP/McuBoot (Flash, chip erase) + HabImage (сборка кастомных HAB), нативный HID (libusbsio) — без Zadig | +| `python-dotenv` | ≥ 1.0 | загрузка `.env` | +| `pyinstaller` | ≥ 6.0 | сборка standalone-бандла (`just host::package-tui`) | -**Runtime-зависимость (не в `pyproject.toml`):** `flasher.py` вызывает -`tools/host/flash_usb.py` через `uv run` — `tools/host/` должен быть -инициализирован (`just host::setup-tools`). +**Никакой рантайм-зависимости на `tools/host/` в смысле subprocess-вызовов +нет.** Прошивка выполняется in-process через `spsdk` (`app/flash_backend.py`). +Из `tools/host/dcd/` читаются только статичные data-блобы (`dcd.bin`, +`*_fdcb.bin`, `ivt_flashloader.bin`) — они отслеживаются в git, `just +host::setup-tools` для запуска TUI из исходников не требуется. `tools/host/ +flash_usb.py` — независимый dev-CLI для `just host::flash*`, TUI его не +вызывает (см. [DEV_ARCH.md](docs/DEV_ARCH.md), §1/§8). --- ## Логирование ```bash -tools/production/service_tui.log ← по умолчанию +tools/production/service_tui.log ← по умолчанию (dev) / рядом с exe (frozen) $SERVICE_LOG_DIR/service_tui.log ← если задан в .env ``` -Уровень: `DEBUG` для модулей приложения, `WARNING` для самого textual. TUI не -пишет в stdout — Textual захватывает терминал. +Уровень по умолчанию — `INFO`; `textual`, `spsdk` и `libusbsio` понижены до +`WARNING` независимо от root (иначе прошивка даёт ~135 строк сырых +HID-пакетов на одну операцию). `SERVICE_LOG_LEVEL=DEBUG` включает полный +DEBUG везде, включая эти модули — используется при разборе проблем на +железе. TUI не пишет в stdout — Textual захватывает терминал. diff --git a/tools/production/docs/DEV_ARCH.md b/tools/production/docs/DEV_ARCH.md index 8e8563f..f402ea9 100644 --- a/tools/production/docs/DEV_ARCH.md +++ b/tools/production/docs/DEV_ARCH.md @@ -14,11 +14,15 @@ ```bash tools/production/ -├── main.py ← точка входа (10 строк) -├── pyproject.toml ← зависимости uv +├── main.py ← точка входа: логирование (Р12) + ServiceApp().run() +├── pyproject.toml ← зависимости uv (включая spsdk==3.7.0) ├── uv.lock +├── service_tui.spec ← PyInstaller spec (Фаза 5, onedir) ├── custom_binaries/ ← runtime, gitignored, создаётся автоматически │ сырые (без FCB/IVT/DCD) бинарники для FlashScreen → «Другое» +├── tests/ +│ └── test_flash_backend.py ← unit-тесты flash_backend.py (45 тестов, без event loop) +├── spike/ ← Фаза 0, де-риск spsdk API (в релиз не идёт) └── app/ ├── app.py ← ServiceApp — роутинг экранов, жизненный цикл клиентов ├── app.tcss ← единый файл стилей для всех экранов @@ -26,7 +30,10 @@ tools/production/ ├── boot_art.py ← LOGO_ART — растеризованный логотип для WaitingScreen ├── firmware_client.py ← async USB CDC клиент firmware_test (UTF-8) ├── m5_client.py ← async M5StampPLC клиент (Serial JSON-lines, UTF-8) - ├── flasher.py ← subprocess-обёртка над tools/host/flash_usb.py + ├── usb_ports.py ← resolve_serial_port() — резолв COM/tty по VID:PID (Р8) + ├── flash_backend.py ← синхронное ядро прошивки: прямой spsdk API (McuBoot/SDP/HabImage), + │ zero Textual/asyncio импортов, тестируется без event loop + ├── flasher.py ← async-обёртка над flash_backend.py (asyncio.to_thread) ├── orchestrator.py ← маршрутизация confirm_request, progress, таймауты ├── widgets/ │ ├── __init__.py @@ -44,6 +51,15 @@ tools/production/ └── confirm_panel.py ← ConfirmPanel — prompt оператора + countdown ``` +> **Разделение dev-CLI / production-TUI:** `tools/host/flash_usb.py` (subprocess +> sdphost/blhost, используется just-рецептами `just host::flash*`) и +> `app/flash_backend.py` (прямой spsdk Python API) — две независимые +> реализации одной и той же логики прошивки. `flash_backend.py` — прямой +> порт `flash_usb.py` на spsdk API (см. заголовок модуля), но TUI больше не +> вызывает `flash_usb.py` ни субпроцессом, ни как библиотеку. `tools/host/` +> используется TUI только как источник статичных data-блобов +> (`tools/host/dcd/*.bin`) в dev-режиме — см. §8. + --- ## 2. Концепция @@ -55,37 +71,48 @@ graph LR subgraph app["app/"] FC["firmware_client.py\nUSB CDC ACM, UTF-8"] M5["m5_client.py\nSerial JSON-lines, UTF-8"] - FL["flasher.py\nsubprocess + pyusb detect"] + FL["flasher.py\nasyncio.to_thread мост"] + FB["flash_backend.py\nspsdk: McuBoot/SDP/HabImage"] OR["orchestrator.py\nconfirm/progress/timeout router"] end TUI --> FC & M5 & FL & OR + FL --> FB end subgraph Board["Плата TFT (MIMXRT1052)"] FW["firmware_test\n(USB CDC)"] ROM["BootROM SDP\n(1FC9:0130)"] + FLD["Flashloader\n(15A2:0073, RAM-резидент)"] end subgraph HIL["HIL стенд (опционально)"] M5HW["M5StampPLC\nRLY1–4 + CAN"] end - subgraph Host["tools/host/"] - FU["flash_usb.py\nsdphost + blhost"] - end - FC <-->|"JSON-lines UTF-8\nVID:PID 1996:00AD"| FW - FL -->|"subprocess uv run"| FU - FU -->|"sdphost + blhost\nVID:PID 1FC9:0130"| ROM + FB -->|"SDP.write_file + jump_and_run\n(spsdk.sdp)"| ROM + ROM -.->|"загружает ivt_flashloader.bin"| FLD + FB -->|"McuBoot: erase/write_memory/reset\n(spsdk.mboot)"| FLD M5 <-->|"JSON-lines\nSerial"| M5HW M5HW -->|"RLY1–4"| Board ``` -> **Детект USB:** `Flasher.detect_sdp()`/`detect_cdc()` используют `pyusb` как -> основной метод (BootROM SDP не создаёт serial-порт на macOS и невидим через -> `pyserial.list_ports`), с fallback на `serial.tools.list_ports` для CDC. -> M5StampPLC детектируется отдельно в `m5_client.py` тем же способом -> (`pyusb`, VID/PID из `.env` — см. раздел 5). +> **Детект USB:** `Flasher.detect_sdp()`/`detect_cdc()` делегируют в +> `flash_backend.detect_sdp()`/`detect_cdc()` (Р7 — `spsdk`-сканеры +> `SdpUSBInterface.scan()`/`serial.tools.list_ports`, БЕЗ `pyusb`: BootROM +> SDP не создаёт serial-порт на macOS, но `spsdk` видит его нативно через +> HID/libusbsio без Zadig на Windows). `usb_ports.resolve_serial_port()` +> резолвит CDC-порты (firmware_test, M5) по VID:PID, а не по имени порта — +> имя не переносимо между перевтыкиваниями (см. Р8, `usb_ports.py`). +> M5StampPLC детектируется отдельно в `m5_client.py` (VID/PID из `.env` — +> см. раздел 5). +> +> **`flash_backend.py` не вызывает `tools/host/flash_usb.py`** ни +> субпроцессом, ни как библиотеку — это прямой порт той же логики на +> spsdk Python API (`McuBoot`/`SDP`/`HabImage` вместо `sdphost`/`blhost`/ +> `nxpimage` CLI), провалидированный байт-в-байт на живом железе (Фаза 0). +> `flash_usb.py` остаётся независимым dev-CLI для `just host::flash*` — +> см. §8 (конвейер сборки) и §6.2 (обработка ошибок прошивки). --- @@ -193,11 +220,14 @@ flowchart TD ## 6. Мониторинг соединения и разрыв сессии +### 6.1 Простой (`ConnectionWatcherMixin`) + `ConnectionWatcherMixin` (`screens/connection_watcher.py`) подключается к `FlashScreen` и `DiagScreen`: каждые 1.5с проверяет, виден ли таргет на шине. - **На `FlashScreen`** — проверка приостановлена во время активной - прошивки/erase (обрыв обнаружит сам `flash_usb.py` subprocess). + прошивки/erase (обрыв в этом случае обнаруживает сам `flash_backend.py`, + см. §6.2, — не watcher). - **На `DiagScreen`** — проверка приостановлена во время прогона тестов (обрыв надёжнее детектирует таймаут чтения порта внутри `Orchestrator`, не просто исчезновение устройства из списка). @@ -214,6 +244,53 @@ flowchart TD разрыва TUI не пытается определить, вернулась ли та же плата, просто стартует диагностику с нуля. +### 6.2 Во время активной операции (`FlashBackendError`, Фаза 4/4a) + +Пока `FlashScreen._flashing == True`, watcher приглушён (§6.1) — обрыв в +этот момент обнаруживает сам `flash_backend.py` через иерархию исключений: + +``` +FlashBackendError (connection_lost: bool = False) +├── ConnectionLostError (connection_lost = True — обёртка +│ над SPSDKConnectionError/SPSDKTimeoutError) +├── DeviceNotFoundError (SDP не найден ДО начала операции) +├── FlashLoaderTimeoutError (Flashloader не поднялся за 10с) +└── HabBuildError (сборка HAB не удалась, к USB не относится) +``` + +- **`_CONNECTION_LOST_EXCEPTIONS = (SPSDKConnectionError, SPSDKTimeoutError)`** + — оба варианта прилетают на обрыве USB (`SPSDKTimeoutError` — потомок + `SPSDKError`, но **не** `SPSDKConnectionError`; read-фаза после write может + отдать голый таймаут вместо connection error). Ловятся кортежем на всех + точках отказа: `load_flashloader`, `flash` (основная + `ram_only` ветки), + `erase_chip`. +- **Вариант B для команд, возвращающих `False` без исключения** (Р10, + `_fail_command()`): `flash_erase_region`/`flash_erase_all`/`write_memory` + иногда просто возвращают `False` вместо исключения. В этом случае + `_fail_command()` выполняет быстрый `_sdp_still_present()` (обёрнутый в + `try/except` — любая ошибка самой проверки трактуется как «устройства + нет», т.к. шина к этому моменту уже нестабильна): устройство пропало → + `ConnectionLostError`, устройство на месте → обычный `FlashBackendError` с + текстом ошибки операции. Проверка добавляется **только в error-путь**, на + happy path не влияет. +- **`Flasher._run_flash_op()`** (`flasher.py`) конвертирует + `FlashBackendError` обратно в `FlashResult(ok=False, + connection_lost=exc.connection_lost)` + событие `FlashProgress(phase="error")`. + Отдельный `except Exception` — safety net на любое непредвиденное + исключение (гарантирует `ok=False` вместо зависших кнопок); `_format_error_message()` + добавляет префикс «Соединение с платой потеряно» для `connection_lost=True`. +- **`FlashScreen`** различает результат (см. docstring `FlashDone`, §9): + `connection_lost=True` → `WaitingScreen` (тот же маркер `target=None`, что + и watcher-детект в простое, текст ошибки прокидывается через + `FlashDone.error_message`); `connection_lost=False` → плата на месте, + экран остаётся на `FlashScreen` (иначе `WaitingScreen` почти мгновенно + переоткрывал бы `FlashScreen` заново и уничтожал `#flash-log` раньше, чем + оператор успевал прочитать сообщение об ошибке). + +USB-интерфейс из `load_flashloader()` закрывается в `finally` +(`_close_iface_quiet`) на любом исходе — защита от утечки HID-хэндла в +редком окне «интерфейс получен → USB выдернут → `McuBoot.__enter__` упал». + --- ## 7. AppFrame — общий каркас экранов @@ -292,47 +369,70 @@ class FlashPreset: подтверждения оператора убирает последний шанс заметить, что в руках не та плата. Оставлена только «липкая» память выбора (этот раздел). -### 8.3 Конвейер сборки (`flasher.py`) +### 8.3 Конвейер сборки — прямые вызовы spsdk (`flash_backend.py`) + +Сборка HAB-образа и прошивка выполняются **in-process** через Python API +`spsdk` — никакого subprocess/CLI (`nxpimage`/`sdphost`/`blhost`), в отличие +от dev-CLI `tools/host/flash_usb.py`, который остаётся отдельной, +независимой реализацией на тех же CLI-утилитах (см. §1, врезка про +разделение dev-CLI/production-TUI). ``` -Flasher.flash(target=CUSTOM, bin_path, use_dcd, fcb_variant, progress_cb) - └── _run_flash_custom() - ├── _build_custom_hab(raw_bin, use_dcd, progress_cb) - │ ├── генерирует temp .yaml в tools/host/hab/ (по образцу hab_bootloader_*.yaml: - │ │ startAddress=0x60000000, ivtOffset=0x1000, initialLoadSize=0x2000, - │ │ family=mimxrt1050, + DCDFilePath: ../dcd/dcd.bin если use_dcd) - │ ├── uv run nxpimage hab export --force -c -o , - │ │ cwd=tools/host/hab/ (обязательно — relative DCDFilePath - │ │ резолвится от этой директории, как в build.just) - │ └── стриминг stdout nxpimage в progress_cb (не только logger.debug — - │ иначе во время сборки лог FlashScreen выглядит «зависшим») - └── flash_usb.py --bin-path --fcb-path tools/host/dcd/{fcb_variant}_fdcb.bin - (временный .yaml и собранный HAB-образ удаляются после прошивки) +Flasher.flash(target=CUSTOM, bin_path, use_dcd, fcb_variant, progress_cb) [flasher.py] + └── _flash_custom() [flasher.py] + ├── asyncio.to_thread(flash_backend.build_custom_hab, raw_bin, use_dcd) + │ ├── _make_hab_config() — генерирует YAML с АБСОЛЮТНЫМИ путями + │ │ во временном work_dir (tempfile.mkdtemp), + DCDFilePath + │ │ на real_dcd_bin_path() если use_dcd + │ ├── Config.create_from_file() + HabImage.get_validation_schemas_from_cfg() + │ │ + cfg.check() — валидация конфига (spsdk.image.hab.hab_image) + │ ├── HabImage.load_from_config(cfg).export() — сборка байт HAB-образа + │ │ в памяти (та же логика, что nxpimage CLI, см. Фазу 0 — golden-тест + │ │ byte-exact, test_build_custom_hab_bytes_match_golden) + │ └── эмитит progress_cb(phase="hab_build", 0% → 100%) + └── asyncio.to_thread(flash_backend.flash, hab_bin, fcb_path=fcb_blob_path(...)) + ├── load_flashloader() — SDP.write_file()+jump_and_run(), ждёт + │ поднятия McuBoot (wait_for_flashloader, poll scan()) + └── with McuBoot(iface): configure_flexspi() → flash_erase_region() + → write_fcb_explicit(fcb_path) → write_memory(hab_bin) + → reset(reopen=False) + (finally: shutil.rmtree(hab_bin.parent) — временная директория + build_custom_hab() удаляется целиком, не только *.bin) ``` `dcd/dcd.bin` — один и тот же файл независимо от проекта (SEMC/SDRAM-init не зависит от того, что именно исполняется), поэтому просто константный путь, -без вариантов. +без вариантов. Пути к data-блобам (`dcd.bin`, `*_fdcb.bin`, +`ivt_flashloader.bin`) резолвятся двухрежимно (`_host_dcd_dir()`, +`flash_backend.py`): в dev — `tools/host/dcd/` (те же файлы, что использует +`flash_usb.py`, отслеживаются в git), в frozen-бандле — +`sys._MEIPASS/data` (см. §14). -### 8.4 `flash_usb.py` — явная запись FCB вместо auto-config +### 8.4 Явная запись FCB вместо auto-config (`write_fcb_explicit`) ```python -def write_fcb_explicit(fcb_path: Path) -> None: - """write-memory 0x60000000 — буквальная запись 512-байтного - FCB-блоба (tag 'FCFB'), а не magic option word 0xF000000F. - Обязателен для кастомных бинарей — auto-config Flashloader проверен - только для W25Q128.""" +def write_fcb_explicit(mboot: McuBoot, fcb_path: Path) -> None: + """Пишет буквальный FCB-блоб (512 байт) в Flash[FLASH_BASE] (custom-бинари). + + См. flash_usb.py::write_fcb_explicit — nxpimage не кладёт FCB в HAB-образ, + поэтому для произвольных чипов нужен явный блоб под конкретный memory chip. + """ ``` -Активируется флагом `--fcb-path` (только вместе с `--bin-path`). Штатный -`--firmware`-путь (три сборки из `BUILD_DIR`) не тронут: без `--fcb-path` -поведение идентично тому, что было до этой доработки. +`mboot.write_memory(FLASH_BASE, data, mem_id=0)` — буквальная запись +512-байтного FCB-блоба (tag `FCFB`), а не magic option word `0xF000000F` +(`write_fcb_auto`, применяется только для штатных +`firmware_test`/`bootloader`/`app`, см. `flash()` в `flash_backend.py`: +параметр `fcb_path=None` → auto-config). Активируется передачей +`fcb_path` в `flash_backend.flash()` — путь без него не тронут: штатная +прошивка идёт через auto-config `write_fcb_auto`, как и раньше. -Заодно увеличен таймаут `blhost` для `flash-erase-all` (chip erase) — -`-t 200000` вместо дефолтного: W25Q512 стирается заметно дольше W25Q128, -дефолтного таймаута `blhost` не хватало. `flash-erase-region` (стирание -пары секторов под FCB+HAB при обычной прошивке) не трогали — там масштаб -на порядки меньше, дефолта достаточно независимо от чипа. +Таймаут для `flash_erase_all` (chip erase) увеличен до `ERASE_ALL_TIMEOUT_MS += 200_000` мс (эквивалент `blhost -t 200000`): W25Q512 стирается заметно +дольше W25Q128, дефолтного таймаута McuBoot не хватало. +`flash_erase_region` (стирание пары секторов под FCB+HAB при обычной +прошивке) не трогали — там масштаб на порядки меньше, дефолта достаточно +независимо от чипа. ### 8.5 UI (`flash.py`) @@ -411,7 +511,7 @@ sequenceDiagram OP->>TUI: запустить service_tui TUI->>WS: push_screen() - WS->>WS: pyusb poll каждые 1.5 с + WS->>WS: spsdk/list_ports poll каждые 1.5 с OP->>FW: подключить плату USB WS->>TUI: DeviceDetected(DIAGNOSING) @@ -547,8 +647,102 @@ runtime-зависимостей `boot_art.py` не добавляет). Есл --- +## 14. Упаковка PyInstaller и frozen-резолв путей (Фаза 5) + +`service_tui.spec` собирает standalone-бандл (onedir — старт быстрее, чем +onefile с распаковкой во временную директорию на каждый запуск). Сборка — +через just-рецепт `just host::package-tui` (не `service-build`, см. +README «Запуск»): запускает PyInstaller, докладывает `firmware//*_hab.bin` +из `BUILD_DIR`, переименовывает результат в `dist/service-tui-vX.Y.Z-/` +(версия — из `pyproject.toml`). + +Целевая структура бандла: + +``` +service-tui-vX.Y.Z-/ +├── service_tui[.exe] +├── _internal/ +│ ├── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin, spsdk data +│ └── ... ← рантайм PyInstaller, libusbsio +├── firmware/ +│ └── /firmware_test_hab.bin +└── custom_binaries/ ← пустая, создаётся оператором/автоматически +``` + +Каждый модуль, которому нужен путь к данным, сам решает dev vs frozen через +`getattr(sys, "frozen", False)` — единообразный паттерн по всему `app/`: + +| Функция | Dev | Frozen | +| --- | --- | --- | +| `flash_backend._host_dcd_dir()` | `tools/host/dcd/` | `sys._MEIPASS/data` | +| `flash_backend.firmware_hab_path()` | `$BUILD_DIR//*_hab.bin` (репо `build/`) | `/firmware//*_hab.bin` | +| `flasher._resolve_custom_binaries_dir()` | `tools/production/custom_binaries/` | `/custom_binaries/` (override — `SERVICE_CUSTOM_BINARIES_DIR`) | +| `waiting._read_app_version()` | `tools/production/pyproject.toml` | тот же путь — `pyproject.toml` кладётся в `datas` спека (нужен для парсинга версии во frozen) | +| `main._setup_logging()` | рядом с `main.py` | рядом с исполняемым файлом (`sys.executable.parent`) | + +`sys.executable` (не `sys._MEIPASS`) — единственный путь, одинаково +работающий и для onefile, и для onedir; `_MEIPASS` для onefile указывает на +временную распаковку, которая исчезает после выхода из процесса. + +Нативный HID-транспорт (`libusbsio`, следствие Р7 — spsdk вместо pyusb) +означает, что `libusb-1.0.*`/Zadig в бандле **не нужны** ни на Windows, ни +на macOS — детект BootROM SDP и Flashloader работает из коробки. + +> **Расхождение spec/факт:** закоммиченный `service_tui.spec` объявляет в +> `datas` только `('../shared', 'shared')` — без `dcd/*.bin`, +> `pyproject.toml` или `spsdk`-данных. Тем не менее уже собранные релизные +> бандлы в `tools/production/dist/service-tui-v0.2.0-{macos,windows}/` +> фактически содержат `_internal/data/{dcd.bin,*_fdcb.bin,ivt_flashloader.bin}`, +> `_internal/pyproject.toml` и `_internal/spsdk/` — то есть сборки, тестировавшиеся +> на железе (Фаза 5, гейт по macOS/Windows), были собраны с более полным +> набором `datas`, чем то, что сейчас лежит в репозитории. `service_tui.spec` +> нужно актуализировать (`collect_data_files("spsdk")`, `tools/host/dcd/*.bin` +> → `data/`, `pyproject.toml`) до следующей сборки релиза — см. «Известные +> открытые вопросы». + +--- + +## 15. Логирование (Р11/Р12, Фаза 4b) + +`main.py::_setup_logging()`: + +- Root-логгер по умолчанию — `INFO` (не `DEBUG`); файл — + `service_tui.log` рядом с исполняемым файлом (или `$SERVICE_LOG_DIR`). +- `SERVICE_LOG_LEVEL=DEBUG` включает полный DEBUG, **включая** портянки + `spsdk`/`libusbsio` (сырые HID TX/RX-пакеты — ~135 строк на одну + прошивку). +- При любом другом значении (или отсутствии переменной) логгеры + `spsdk`, `libusbsio`, `libusbsio.hidapi.dev`, + `spsdk.mboot.protocol.bulk_protocol` принудительно приглушены до + `WARNING`, независимо от уровня root — иначе диагностика `app.*` + тонет в чужом протоколе. +- `textual` отдельно всегда на `WARNING`. + +`FlashScreen._on_progress()` (Р11) троттлит **только** запись в +`#flash-log` для фазы `write`: событие логируется раз на каждые 10% +(`progress.percent // 10`), а не на каждый пакет `spsdk` (~135 → +~10 строк). Прогресс-бар при этом обновляется на **каждом** событии — +плавность не теряется, троттлинг влияет только на текстовый лог. + +--- + ## Известные открытые вопросы +- **Документация — Фаза 6 (текущая).** Инженерные фазы 0–5 (backend на spsdk, + обработка обрыва USB, троттлинг логов, упаковка PyInstaller) закрыты в + коде; `docs/DEV_ARCH.md`/`README.md` актуализированы этой правкой. Осталось + по `RELEASE_ROADMAP.md` §Фаза 6: `CHANGELOG.md` (не заведён), grep-зачистка + устаревших docstring-упоминаний `flash_usb.py`/`subprocess` в + `app/flash.py` (комментарий `_check_sdp_present`) и `flasher.py` + (docstring модуля упоминает Фазу 2 буквально, что нормально как история + провенанса, но стоит перепроверить при следующей правке этих файлов). +- **`service_tui.spec` не актуализирован под реальные релизные сборки** — + см. §14. Нужно добавить `datas` (`dcd/*.bin`, `pyproject.toml`, + `collect_data_files("spsdk")`) до следующей упаковки релиза. +- **`pyusb` в `pyproject.toml` — мёртвая зависимость.** Р7 перевёл детект + SDP/CDC на `spsdk`/`serial.tools.list_ports`; ни один модуль `app/` больше + не импортирует `usb`/`pyusb`. Кандидат на удаление при следующей + grep-зачистке (Фаза 6). - **Release-сборка firmware нестабильна** (медленное мигание — подозрение на проблему с FCB/clock конфигурацией в Release HAB-образе) — TUI временно форсирует Debug через `FIRMWARE_BUILD_TYPE`. @@ -557,14 +751,19 @@ runtime-зависимостей `boot_art.py` не добавляет). Есл архитектурным решением, а не техдолгом. - Пункты плана TUI «экспорт результатов в JSON с привязкой к UID» и «копирование UID с экрана» — отложены, не начаты. +- **POST-1 (циклический прогон неинтерактивных тестов на DiagScreen)** — + сознательно отложен на пост-релиз, вне `MONOLITH_APP_PLAN.md` (см. + `RELEASE_ROADMAP.md`). - **Массовое программирование** — решено НЕ делать авто-прошивку по факту детекта SDP (см. §8.2); ограничились «липким» `FlashPreset`. Если в будущем понадобится полный батч-режим — потребуется отдельный предохранитель (задержка с отменой перед стартом), т.к. в SDP-режиме плату нельзя - идентифицировать по UID. + идентифицировать по UID. Связанное ограничение v1 (О3) — предполагается + ровно одна плата на столе одновременно (см. README). - **Auto-config Flashloader для W25Q256/512 не проверялся напрямую** — решили не полагаться на него вообще, для кастомных бинарей FCB всегда пишется - явно (`--fcb-path`, см. §8.4). Остаётся не до конца понятым, работает ли - `configure-memory 0xF000000F` для этих чипов корректно в принципе — вопрос - снят с повестки архитектурным решением, а не исследован до конца. + явно (параметр `fcb_path` в `flash_backend.flash()`, см. §8.4). Остаётся не + до конца понятым, работает ли `configure-memory 0xF000000F` для этих + чипов корректно в принципе — вопрос снят с повестки архитектурным + решением, а не исследован до конца. \ No newline at end of file