# Refactoring: all the documents
This commit is contained in:
parent
22c40779ef
commit
0f54c35be6
16 changed files with 758 additions and 436 deletions
189
CHANGELOG.md
189
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` + незакоммиченные изменения рабочего дерева (документация)
|
||||
Сравнение: <https://github.com/OSabuser/tft_manufacture_test/compare/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_<n>` без `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
|
||||
|
||||
|
|
|
|||
21
README.md
21
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-зависимостей).
|
||||
|
|
|
|||
|
|
@ -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(); }
|
||||
|
||||
|
|
|
|||
|
|
@ -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 усиление равно нулю.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 остаётся открытым после вызова (паттерн проекта).
|
||||
|
|
|
|||
|
|
@ -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()` возвращает фактически прочитанное количество байт —
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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`) |
|
||||
|
||||
---
|
||||
|
||||
## Матрица тестов
|
||||
|
||||
Порядок — как в реестре `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 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 | ❌ | ✅ | ❌ |
|
||||
| `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:"<id> 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#как-добавить-новый-тест).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -166,12 +166,18 @@ ls build/target-debug/tests/target/<name>/test_<name>.elf
|
|||
|
||||
## Шаг 5 — `conftest.py`: добавить фикстуры
|
||||
|
||||
### Базовый тест (без M5)
|
||||
### Любой тест — фикстура загрузки всегда зависит от `m5`
|
||||
|
||||
M5StampPLC управляет питанием таргета (RLY1 → VIN, см. `HIL_BENCH.md`), а
|
||||
не только сигнальными реле — поэтому `loaded_<n>` зависит от `m5` **во всех
|
||||
случаях**, даже если сам тест не использует реле для сигналов (например,
|
||||
`01_test_uart.py`/`loaded_host_uart`). Без этой зависимости pyOCD попытается
|
||||
подключиться к обесточенной плате.
|
||||
|
||||
```python
|
||||
# 1. Фикстура загрузки
|
||||
# 1. Фикстура загрузки — m5 гарантирует, что питание включено до pyOCD
|
||||
@pytest.fixture(scope="module")
|
||||
def loaded_<n>(request: pytest.FixtureRequest) -> None:
|
||||
def loaded_<n>(request: pytest.FixtureRequest, m5: M5Agent) -> None:
|
||||
_load_elf(
|
||||
request,
|
||||
Path(cfg.BUILD_DIR) / "tests/target/<n>/test_<n>.elf",
|
||||
|
|
@ -184,26 +190,10 @@ _UART_FIXTURE_MAP = {
|
|||
}
|
||||
```
|
||||
|
||||
### Тест с M5
|
||||
|
||||
```python
|
||||
# 1. Зависимость от m5 гарантирует что питание включено до загрузки ELF
|
||||
@pytest.fixture(scope="module")
|
||||
def loaded_<n>(request: pytest.FixtureRequest, m5: M5Agent) -> None:
|
||||
_load_elf(
|
||||
request,
|
||||
Path(cfg.BUILD_DIR) / "tests/target/<n>/test_<n>.elf",
|
||||
)
|
||||
|
||||
# 2. UART-фикстура — та же одна строка
|
||||
_UART_FIXTURE_MAP = {
|
||||
...
|
||||
"uart_<n>": "loaded_<n>",
|
||||
}
|
||||
```
|
||||
|
||||
**Правило:** если тест управляет железом через M5 — `loaded_<n>` должен явно
|
||||
зависеть от `m5`, иначе pyOCD попытается подключиться до включения питания.
|
||||
Различие между «базовым» и «с M5» тестом — не в сигнатуре `loaded_<n>`
|
||||
(она всегда одна и та же), а в том, использует ли сам **тест-кейс**
|
||||
`m5.opto_set()`/`m5.relay_set()`/`m5.can_*()` для управления сигналами
|
||||
помимо включения питания (см. пример «Тест с M5» в Шаге 6 ниже).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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/<name>/
|
||||
touch tests/host/<name>/test_<name>.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/<module>.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_<name>
|
||||
SOURCES <name>/test_<name>.c
|
||||
${PROJECT_SOURCE_DIR}/<путь-к-модулю>/<module>.c
|
||||
INCLUDES ${PROJECT_SOURCE_DIR}/<путь-к-инклюдам>
|
||||
)
|
||||
|
||||
# категория B — BSP-модуль, нужны MOCKS
|
||||
add_host_test(
|
||||
NAME test_<name>
|
||||
SOURCES <name>/test_<name>.c
|
||||
${PROJECT_SOURCE_DIR}/bsp/<name>/src/<name>.c
|
||||
INCLUDES ${PROJECT_SOURCE_DIR}/bsp/<name>/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_<name> 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_<name> -V
|
||||
|
||||
# 4. DevContainer — VSCode
|
||||
# Ctrl+Shift+D → 🐛 Debug: firmware_test → F5
|
||||
# напрямую — без обёртки CTest
|
||||
./build/host-debug/tests/host/test_<name>
|
||||
```
|
||||
|
||||
`just build::test-host` собирает под пресетом `host-debug` (`clang-17`,
|
||||
без ARM-специфики) и прогоняет весь набор через CTest. `host-release`
|
||||
собирает тот же набор с оптимизациями — используется в CI как
|
||||
дополнительный гейт.
|
||||
|
||||
---
|
||||
|
||||
## Чеклист
|
||||
|
||||
```bash
|
||||
[ ] tests/host/<name>/test_<name>.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).
|
||||
|
|
|
|||
|
|
@ -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-<os>/
|
||||
```
|
||||
|
||||
> 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) |
|
||||
| `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-бинаря |
|
||||
| `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 захватывает терминал.
|
||||
|
|
|
|||
|
|
@ -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 <yaml> -o <out>,
|
||||
│ │ cwd=tools/host/hab/ (обязательно — relative DCDFilePath
|
||||
│ │ резолвится от этой директории, как в build.just)
|
||||
│ └── стриминг stdout nxpimage в progress_cb (не только logger.debug —
|
||||
│ иначе во время сборки лог FlashScreen выглядит «зависшим»)
|
||||
└── flash_usb.py --bin-path <hab_bin> --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 <fcb_path> — буквальная запись 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/<Type>/*_hab.bin`
|
||||
из `BUILD_DIR`, переименовывает результат в `dist/service-tui-vX.Y.Z-<os>/`
|
||||
(версия — из `pyproject.toml`).
|
||||
|
||||
Целевая структура бандла:
|
||||
|
||||
```
|
||||
service-tui-vX.Y.Z-<os>/
|
||||
├── service_tui[.exe]
|
||||
├── _internal/
|
||||
│ ├── data/ ← dcd.bin, *_fdcb.bin, ivt_flashloader.bin, spsdk data
|
||||
│ └── ... ← рантайм PyInstaller, libusbsio
|
||||
├── firmware/
|
||||
│ └── <Type>/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/<Type>/*_hab.bin` (репо `build/`) | `<exe_dir>/firmware/<Type>/*_hab.bin` |
|
||||
| `flasher._resolve_custom_binaries_dir()` | `tools/production/custom_binaries/` | `<exe_dir>/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` для этих
|
||||
чипов корректно в принципе — вопрос снят с повестки архитектурным
|
||||
решением, а не исследован до конца.
|
||||
|
||||
Loading…
Reference in a new issue