# Simple usb-cdc cli + host tests
This commit is contained in:
parent
6f0e082849
commit
5f2876e89f
13 changed files with 1472 additions and 555 deletions
16
.vscode/tasks.json
vendored
16
.vscode/tasks.json
vendored
|
|
@ -31,7 +31,7 @@
|
|||
// HOST-ТЕСТЫ
|
||||
// =============================================================
|
||||
{
|
||||
"label": "🧪 Host Tests (Debug)",
|
||||
"label": "Host Tests",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
|
|
@ -45,14 +45,14 @@
|
|||
"cwd": "${workspaceFolder}",
|
||||
"statusbar": {
|
||||
"color": "#c800ff",
|
||||
"label": "$(test-view-icon) Host Tests",
|
||||
"label": "$(test-view-icon) Host Tests (Debug)",
|
||||
"detail": "Run host unit tests"
|
||||
}
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "🧪 Host Tests (Release)",
|
||||
"label": "🔨Host Tests (Release)",
|
||||
"group": "test",
|
||||
"type": "shell",
|
||||
"command": "just",
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
"cwd": "${workspaceFolder}",
|
||||
"statusbar": {
|
||||
"color": "#00e5ff",
|
||||
"label": "$(circuit-board) HIL Build",
|
||||
"label": "$(circuit-board) HIL Tests (Debug)",
|
||||
"detail": "Build HIL target firmware"
|
||||
}
|
||||
},
|
||||
|
|
@ -91,7 +91,7 @@
|
|||
// HAB-ОБРАЗЫ
|
||||
// =============================================================
|
||||
{
|
||||
"label": "📦 HAB Image",
|
||||
"label": "HAB Image",
|
||||
"group": "build",
|
||||
"type": "shell",
|
||||
"command": "just",
|
||||
|
|
@ -102,14 +102,14 @@
|
|||
"cwd": "${workspaceFolder}",
|
||||
"statusbar": {
|
||||
"color": "#03b3ff",
|
||||
"label": "$(package) HAB",
|
||||
"label": "$(package) HAB Image Build",
|
||||
"detail": "Build HAB image"
|
||||
}
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "📦 HAB All (Debug)",
|
||||
"label": "📦 HAB Build All (Debug)",
|
||||
"group": "build",
|
||||
"type": "shell",
|
||||
"command": "just",
|
||||
|
|
@ -122,7 +122,7 @@
|
|||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "📦 HAB All (Release)",
|
||||
"label": "📦 HAB Build All (Release)",
|
||||
"group": "build",
|
||||
"type": "shell",
|
||||
"command": "just",
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@
|
|||
"test_bsp_can",
|
||||
"uart_host_mock_example",
|
||||
"test_ring_buffer",
|
||||
"test_cli",
|
||||
"test_timeout_pattern",
|
||||
"uart_host_mock_example"
|
||||
]
|
||||
|
|
@ -160,6 +161,7 @@
|
|||
"test_bsp_opto",
|
||||
"test_bsp_button",
|
||||
"test_bsp_can",
|
||||
"test_cli",
|
||||
"uart_host_mock_example",
|
||||
"test_ring_buffer",
|
||||
"test_timeout_pattern",
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@
|
|||
|
||||
set(TARGET_NAME firmware_test)
|
||||
|
||||
add_executable(${TARGET_NAME} main.c ${BSP_GENERATED}/clock_config.c
|
||||
${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE})
|
||||
add_executable(
|
||||
${TARGET_NAME} src/main.c src/cli.c ${BSP_GENERATED}/clock_config.c
|
||||
${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE})
|
||||
|
||||
target_include_directories(firmware_test PRIVATE src/)
|
||||
|
||||
target_compile_definitions(${TARGET_NAME}
|
||||
PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512)
|
||||
|
|
@ -14,16 +17,8 @@ target_compile_definitions(${TARGET_NAME}
|
|||
# даёт: sdk_device, sdk_clock, sdk_common, CPU_MIMXRT1052CVJ5B, XIP_* дефайны
|
||||
# -----------------------------------------------------------------------------
|
||||
target_link_libraries(
|
||||
${TARGET_NAME}
|
||||
PRIVATE bsp_board
|
||||
bsp_led
|
||||
bsp_tick
|
||||
bsp_uart_host
|
||||
bsp_boot_xip
|
||||
bsp_usb_cdc
|
||||
port_log_uart
|
||||
lib_external # SEGGER RTT если включён через SEGGER_RTT_ENABLED
|
||||
)
|
||||
${TARGET_NAME} PRIVATE bsp_board bsp_led bsp_tick bsp_boot_xip bsp_status
|
||||
bsp_usb_cdc)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Linker script
|
||||
|
|
|
|||
559
firmware/test/PLAN.md
Normal file
559
firmware/test/PLAN.md
Normal file
|
|
@ -0,0 +1,559 @@
|
|||
# firmware_test — Plan of Development
|
||||
|
||||
> Документ для нового треда. Содержит все принятые решения, текущий статус и
|
||||
> пошаговый план дальнейшей разработки.
|
||||
> Версия: 0.2 | Обновлён после реализации скелета + bsp_usb_cdc.
|
||||
|
||||
---
|
||||
|
||||
## Контекст проекта
|
||||
|
||||
**Цель прошивки:** входной контроль платы MIMXRT1052CVJ5B на производстве.
|
||||
Запускается через BootROM (USB SDP), без предварительной прошивки загрузчика.
|
||||
|
||||
**Стенд:**
|
||||
- Хост подключается через USB CDC ACM (J2) — единственный канал
|
||||
- Тесты с внешними сигналами управляются через M5StampPLC (реле, оптовходы)
|
||||
- Отдельный компьютер-сервер запускает pytest
|
||||
|
||||
---
|
||||
|
||||
## Статус на момент создания документа
|
||||
|
||||
| Компонент | Статус | Примечание |
|
||||
|---|---|---|
|
||||
| `bsp_usb_cdc` | ✅ Готов | HIL тест пройден (`05_test_usb_cdc.py`) |
|
||||
| `firmware_test` скелет | ✅ Готов | `main.c` + `cli.c` + PING работает |
|
||||
| Host-тест CLI | ✅ Готов | `test_cli.c`, 8 тестов, зелёные |
|
||||
| `bsp_sdram` | ⬜ Не начат | |
|
||||
| `bsp_qspi` | ⬜ Не начат | |
|
||||
| `bsp_usd` | ⬜ Не начат | |
|
||||
| Протокол v2 | ⬜ Не начат | Эволюция от cmd/ok к event-driven |
|
||||
| Test runner | ⬜ Не начат | |
|
||||
| Provisioning | ⬜ Не начат | |
|
||||
|
||||
### Закрытые архитектурные решения
|
||||
|
||||
> Не пересматривать без явного запроса.
|
||||
|
||||
- **[DECISION] Транспорт:** USB CDC ACM — единственный канал. UART не используется в firmware_test.
|
||||
- **[DECISION] Парсинг JSON:** без cJSON, строковый `strstr`. Входящее поле всегда `"cmd"` / `"type"`.
|
||||
- **[DECISION] SDRAM и DCD:** SEMC инициализируется DCD до `main()`. `bsp_sdram_init()` только верифицирует.
|
||||
- **[DECISION] SDRAM тест — не HIL ELF:** тест прогоняется командами через firmware_test, не отдельным ELF.
|
||||
- **[DECISION] IR и RTC:** не реализуются.
|
||||
|
||||
---
|
||||
|
||||
## Протокол v2 — решение, требующее принятия в новом треде
|
||||
|
||||
Текущий скелет использует упрощённый протокол:
|
||||
```json
|
||||
{"cmd":"PING"} → {"ok":true,"result":"PONG"}
|
||||
```
|
||||
|
||||
TODO.md описывает расширенный протокол с типами событий:
|
||||
```json
|
||||
{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
|
||||
{"type":"test_begin","id":"sdram","name":"SDRAM 32MB","critical":true}
|
||||
{"type":"test_result","id":"sdram","status":"pass","ms":312}
|
||||
{"type":"summary","passed":7,"failed":0,"overall":"pass"}
|
||||
```
|
||||
|
||||
**Вопросы для обсуждения в треде:**
|
||||
|
||||
1. Переходить ли на v2 сразу или итерационно (сначала SDRAM с cmd/ok, потом рефакторинг)?
|
||||
2. Как обрабатывать `confirm_request` (display test) в cli.c — отдельный тип входящего сообщения?
|
||||
3. `session_start` — посылать ли при каждом `READY` или только по команде `START_SESSION`?
|
||||
|
||||
**Рекомендация:** реализовать v2 до добавления первого теста, иначе рефакторинг CLI затронет уже написанные тест-модули.
|
||||
|
||||
---
|
||||
|
||||
## Структура файлов — целевое состояние
|
||||
```
|
||||
firmware/test/
|
||||
├── CMakeLists.txt
|
||||
├── README.md
|
||||
└── src/
|
||||
├── main.c # ✅ готов
|
||||
├── cli.h / cli.c # ✅ готов (v1), требует эволюции до v2
|
||||
├── protocol.h / protocol.c # ⬜ новый: сериализация ответов
|
||||
├── test_runner.h / .c # ⬜ новый: реестр + sequencer
|
||||
├── provisioning.h / .c # ⬜ новый: chip UID + provision_ack
|
||||
└── tests/
|
||||
├── test_sdram.h / .c # ⬜
|
||||
├── test_qspi.h / .c # ⬜
|
||||
├── test_usd.h / .c # ⬜
|
||||
├── test_display.h / .c # ⬜ интерактивный (кнопки)
|
||||
├── test_can.h / .c # ⬜ HIL (M5StampPLC)
|
||||
├── test_uart.h / .c # ⬜ HIL TTL + ISO
|
||||
└── test_opto.h / .c # ⬜ HIL (M5StampPLC)
|
||||
|
||||
bsp/
|
||||
├── sdram/ # ⬜ CMakeLists.txt + sdram.c (структура есть)
|
||||
├── qspi/ # ⬜ новый модуль
|
||||
└── usd/ # ⬜ новый модуль
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Матрица тестов
|
||||
|
||||
| ID | Название | Тип | Critical | HIL (M5) | HIL ELF | BSP | Статус |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| — | PING | cmd | — | — | — | — | ✅ |
|
||||
| `sdram` | SDRAM 32MB | self | ✅ | ❌ | ❌ | `bsp_sdram` | ⬜ |
|
||||
| `qspi` | QSPI Flash | self | ✅ | ❌ | ❌ | `bsp_qspi` | ⬜ |
|
||||
| `usd` | uSD (SDIO) | self | ✅ | ❌ | ❌ | `bsp_usd` | ⬜ |
|
||||
| `display` | Display RGB888 | interactive | ❌ | ❌ | ❌ | существующий BSP | ⬜ |
|
||||
| `can` | CAN | HIL | ❌ | ✅ | ❌ | `bsp_can` ✅ | ⬜ |
|
||||
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ | `bsp_uart_host` ✅ | ⬜ |
|
||||
| `uart_iso` | UART ISO +24V | HIL | ❌ | ✅ | ❌ | уточнить | ⬜ |
|
||||
| `opto` | Opto-in +24V | HIL | ❌ | ✅ | ❌ | `bsp_opto` ✅ | ⬜ |
|
||||
|
||||
**Колонка "HIL ELF":** отдельная RAM-прошивка через pyOCD. Для firmware_test тестов — не нужна, тесты идут через USB CDC.
|
||||
|
||||
**Колонка "HIL (M5)":** нужна ли M5StampPLC для управления внешними сигналами.
|
||||
|
||||
---
|
||||
|
||||
## Пошаговый план
|
||||
|
||||
### Этап 0 — Вопросы, требующие ответа до кода
|
||||
|
||||
Обсудить в начале треда:
|
||||
|
||||
- Протокол v2: переходить сейчас или после SDRAM?
|
||||
- `uart_iso`: есть отдельный BSP модуль или это тот же `bsp_uart_host` с другими параметрами?
|
||||
- `usd`: карта вставлена постоянно на плате или оператор вставляет перед тестом?
|
||||
- QSPI: какой конкретно чип (W25Q128?), есть ли уже sdk_flexspi таргет в CMake?
|
||||
|
||||
---
|
||||
|
||||
### Этап 1 — Протокол v2 + Test runner скелет
|
||||
|
||||
**Цель:** эволюция cli.c → protocol.c + test_runner.c. После этого этапа добавление каждого теста — одна строка в реестре.
|
||||
|
||||
**Файлы:**
|
||||
```
|
||||
firmware/test/src/
|
||||
├── cli.c ← упрощается: только IO (read/write/buffer)
|
||||
├── protocol.h/.c ← новый: сериализация всех типов сообщений
|
||||
├── test_module.h ← новый: интерфейс тест-модуля
|
||||
└── test_runner.h/.c ← новый: реестр + sequencer
|
||||
```
|
||||
|
||||
**Интерфейс тест-модуля (`test_module.h`):**
|
||||
```c
|
||||
typedef enum { TEST_STATUS_PASS = 0, TEST_STATUS_FAIL, TEST_STATUS_SKIP } test_status_t;
|
||||
|
||||
typedef struct {
|
||||
test_status_t status;
|
||||
uint32_t duration_ms;
|
||||
char detail[96];
|
||||
} test_result_t;
|
||||
|
||||
typedef struct {
|
||||
const char *id;
|
||||
const char *name;
|
||||
bool critical;
|
||||
bool requires_hil;
|
||||
void (*init)(void);
|
||||
test_result_t (*run)(void);
|
||||
void (*deinit)(void);
|
||||
} test_module_t;
|
||||
```
|
||||
|
||||
**Сериализация (protocol.h):** функции `protocol_send_session_start()`,
|
||||
`protocol_send_test_begin()`, `protocol_send_test_result()`,
|
||||
`protocol_send_summary()`, `protocol_send_confirm_request()` — все через `cli_send()`.
|
||||
|
||||
**Команды хоста v2:**
|
||||
|
||||
| Входящий тип | Поле | Действие |
|
||||
|---|---|---|
|
||||
| `cmd` | `"run_all"` | Запустить все тесты по реестру |
|
||||
| `cmd` | `"run"` + `"id"` | Запустить один тест |
|
||||
| `cmd` | `"ping"` | `{"type":"pong"}` |
|
||||
| `confirm` | `"id"` + `"confirmed"` | Ответ оператора на display тест |
|
||||
|
||||
**Host-тесты:** расширить `test_cli.c` + добавить `test_protocol.c` (Категория A).
|
||||
|
||||
---
|
||||
|
||||
### Этап 2 — `bsp_sdram`
|
||||
|
||||
API уже описан в `firmware_test_plan.md` (закрытое решение):
|
||||
```c
|
||||
bsp_sdram_status_t bsp_sdram_init(void);
|
||||
bsp_sdram_status_t bsp_sdram_test_fast(bsp_sdram_result_t *result);
|
||||
bsp_sdram_status_t bsp_sdram_test_full(bsp_sdram_result_t *result);
|
||||
```
|
||||
|
||||
**Файлы:**
|
||||
```
|
||||
bsp/sdram/
|
||||
├── CMakeLists.txt ← создать
|
||||
├── include/bsp/sdram.h ← по spec из firmware_test_plan.md
|
||||
└── src/sdram.c
|
||||
```
|
||||
|
||||
**Тест модуль firmware_test:**
|
||||
```
|
||||
firmware/test/src/tests/test_sdram.c
|
||||
```
|
||||
|
||||
Команды через реестр: `run` + `id: "sdram"`. Отдельной HIL ELF нет.
|
||||
|
||||
**HIL pytest (`tools/hil/test_sdram.py`):** подключается к firmware_test через USB CDC.
|
||||
Фикстура `cdc_firmware_test` — firmware_test прошит в Flash, pytest открывает CDC порт.
|
||||
|
||||
**Just рецепты:** `hil-sdram` (без `--slow`), `hil-sdram-full` (с `@pytest.mark.slow`).
|
||||
|
||||
---
|
||||
|
||||
### Этап 3 — `bsp_qspi` + QSPI тест
|
||||
|
||||
**Аппаратный контекст:** W25Q64FVSSIG (из схемы, 8MB SPI NOR Flash), интерфейс FlexSPI.
|
||||
|
||||
**API:**
|
||||
```c
|
||||
bsp_qspi_status_t bsp_qspi_init(void);
|
||||
bsp_qspi_status_t bsp_qspi_read_jedec_id(uint8_t *manufacturer, uint16_t *device_id);
|
||||
bsp_qspi_status_t bsp_qspi_test(bsp_qspi_result_t *result); /* erase sector + write + verify */
|
||||
```
|
||||
|
||||
**Что проверяет тест:**
|
||||
|
||||
1. JEDEC ID совпадает с ожидаемым для W25Q64 (`0xEF`, `0x4017`)
|
||||
2. Erase тестового сектора (последний сектор, чтобы не трогать прошивку)
|
||||
3. Write + Read + Compare 256 байт
|
||||
|
||||
**Нет HIL ELF, нет M5.** Тест полностью самостоятельный.
|
||||
|
||||
**Вопрос перед началом:** проверить есть ли `sdk_flexspi` таргет в `sdk/CMakeLists.txt`.
|
||||
|
||||
---
|
||||
|
||||
### Этап 4 — `bsp_usd` + uSD тест
|
||||
|
||||
**Аппаратный контекст:** SDMMC (uSD слот), интерфейс USDHC. SDK таргет `sdk_usdhc` есть в матрице.
|
||||
|
||||
**Стратегия:** использовать FatFS из SDK middleware (уже vendored в `middleware/fatfs/`).
|
||||
|
||||
**API:**
|
||||
```c
|
||||
bsp_usd_status_t bsp_usd_init(void); /* USDHC init + mount FAT */
|
||||
bsp_usd_status_t bsp_usd_test(bsp_usd_result_t *result); /* write + read + verify */
|
||||
bsp_usd_status_t bsp_usd_deinit(void); /* unmount */
|
||||
```
|
||||
|
||||
**Поведение при отсутствии карты:** `TEST_STATUS_SKIP` (критически важно для
|
||||
производственного прогона — карта может быть не вставлена).
|
||||
|
||||
**Вопрос:** карта вставлена постоянно или оператор вставляет? Если оператор — нужен `confirm_request` перед тестом.
|
||||
|
||||
---
|
||||
|
||||
### Этап 5 — Display тест (интерактивный)
|
||||
|
||||
**Что проверяем:** RGB888 интерфейс + подсветка + реакция оператора через кнопки.
|
||||
|
||||
**Последовательность:**
|
||||
```
|
||||
firmware → LCD: залить R (красный)
|
||||
firmware → хост: {"type":"confirm_request","id":"display_red","timeout_ms":15000}
|
||||
оператор: нажать кнопку PASS (Test_But_1) или FAIL (Test_But_2)
|
||||
firmware → хост: {"type":"confirm_ack","id":"display_red","confirmed":true/false}
|
||||
повторить для G, B, W
|
||||
итоговый результат = AND всех подтверждений
|
||||
```
|
||||
|
||||
**Особенности:**
|
||||
|
||||
- Одновременно тестируются кнопки (`bsp_button` уже есть с HIL тестами)
|
||||
- Таймаут 15 с → `TEST_STATUS_SKIP`
|
||||
- Маркер `@pytest.mark.interactive` в pytest — не входит в `hil-run`
|
||||
|
||||
**HIL pytest:** `tools/hil/test_display.py` с `_operator_prompt()` через `/dev/tty`.
|
||||
|
||||
---
|
||||
|
||||
### Этап 6 — HIL тесты: CAN, UART, Opto в firmware_test
|
||||
|
||||
Эти BSP модули уже реализованы и имеют отдельные HIL ELF тесты.
|
||||
Задача этапа — **интегрировать их в firmware_test** как тест-модули,
|
||||
запускаемые через протокол v2.
|
||||
|
||||
#### 6.1 CAN (`bsp_can` ✅)
|
||||
|
||||
- Стенд: M5StampPLC подключён к CAN шине платы через интерфейсную плату
|
||||
- Тест: M5 посылает CAN фрейм → плата принимает → сравниваем
|
||||
|
||||
#### 6.2 UART TTL (`bsp_uart_host` ✅)
|
||||
|
||||
- Стенд: M5 UART ↔ UART TTL платы (loopback или echo)
|
||||
- Уточнить: какой UART порт на плате (LPUART1 занят MCU-Link, какой свободен?)
|
||||
|
||||
#### 6.3 UART ISO
|
||||
|
||||
- Уточнить наличие отдельного BSP модуля или это конфигурация `bsp_uart_host`
|
||||
- +24V уровни через интерфейсную плату
|
||||
|
||||
#### 6.4 Opto (`bsp_opto` ✅)
|
||||
|
||||
- Стенд: M5 реле → оптовходы EXT_IN1, EXT_IN2, RS_RX
|
||||
- Логика уже отработана в `02_test_opto.py` (HIL ELF)
|
||||
- Переиспользовать: тот же M5 агент, другой транспорт (USB CDC вместо UART)
|
||||
|
||||
**Важно для всех HIL тестов этапа 6:** pytest для firmware_test использует
|
||||
`cdc_firmware_test` фикстуру (USB CDC), а не `uart_<n>` (UART + pyOCD).
|
||||
M5StampPLC управляет сигналами так же, как в существующих HIL ELF тестах.
|
||||
|
||||
---
|
||||
|
||||
### Этап 7 — Provisioning
|
||||
|
||||
**Источник UID:** OCOTP регистры через `OCOTP_GetFuseData()` (NXP HAL).
|
||||
```c
|
||||
void provisioning_run(provision_info_t *out);
|
||||
```
|
||||
|
||||
**Поток:**
|
||||
```
|
||||
firmware → хост: {"type":"provision_ready","chip_uid":"A3F2...","fw":"0.1.0"}
|
||||
хост → БД: uid ↔ fw_version (логика на хосте)
|
||||
хост → firmware: {"type":"cmd","cmd":"provision_ack","fw":"1.0.0","bootloader":"1.0.0"}
|
||||
firmware → хост: {"type":"provision_done","recorded":true}
|
||||
```
|
||||
|
||||
**Запускается только при `overall == pass`.** В pytest отдельная фикстура
|
||||
`provision_firmware_test`.
|
||||
|
||||
---
|
||||
|
||||
## Зависимости между этапами
|
||||
```
|
||||
Этап 1 (протокол v2 + runner)
|
||||
├── Этап 2 (bsp_sdram)
|
||||
│ └── HIL: test_sdram.py
|
||||
├── Этап 3 (bsp_qspi)
|
||||
│ └── HIL: test_qspi.py
|
||||
├── Этап 4 (bsp_usd)
|
||||
│ └── HIL: test_usd.py
|
||||
├── Этап 5 (display, interactive)
|
||||
│ └── HIL: test_display.py (@pytest.mark.interactive)
|
||||
└── Этап 6 (CAN + UART + Opto)
|
||||
└── HIL: test_can.py, test_uart.py, test_opto.py
|
||||
|
||||
Этап 7 (provisioning) → зависит от всех предыдущих
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## BSP модули — итог
|
||||
|
||||
| BSP | Статус | Нужен HIL ELF | Нужен в firmware_test |
|
||||
|---|---|---|---|
|
||||
| `bsp_usb_cdc` | ✅ | ✅ (есть) | ✅ (есть) |
|
||||
| `bsp_led` | ✅ | ❌ | ✅ |
|
||||
| `bsp_tick` | ✅ | ❌ | ✅ |
|
||||
| `bsp_button` | ✅ | ✅ (есть) | ✅ (display тест) |
|
||||
| `bsp_opto` | ✅ | ✅ (есть) | ✅ этап 6 |
|
||||
| `bsp_can` | ✅ | ✅ (есть) | ✅ этап 6 |
|
||||
| `bsp_uart_host` | ✅ | ✅ (есть) | ✅ этап 6 |
|
||||
| `bsp_sdram` | ⬜ | ❌ | ✅ этап 2 |
|
||||
| `bsp_qspi` | ⬜ | ❌ | ✅ этап 3 |
|
||||
| `bsp_usd` | ⬜ | ❌ | ✅ этап 4 |
|
||||
|
||||
---
|
||||
|
||||
## Контекст для нового треда — что передать
|
||||
```
|
||||
Системный промпт: тот же (роль + правила).
|
||||
|
||||
Приложить файлы:
|
||||
- firmware_test_plan.md (обновлённый)
|
||||
- usb_cdc.md
|
||||
- firmware/test/src/main.c (текущий)
|
||||
- firmware/test/src/cli.h/.c (текущий)
|
||||
- firmware/test/CMakeLists.txt (текущий)
|
||||
- tests/host/CMakeLists.txt
|
||||
- tests/host/cli/test_cli.c
|
||||
- этот план (PLAN.md)
|
||||
|
||||
Первый вопрос нового треда:
|
||||
"Начинаем Этап 1 — протокол v2 и test_runner.
|
||||
Ответы на открытые вопросы: [...]"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Открытый вопрос: ПО на стороне хоста для производственного прогона
|
||||
|
||||
### Контекст и разделение ответственности
|
||||
|
||||
Принципиально важно разделить два окружения:
|
||||
|
||||
| Окружение | Кто запускает | Инструмент | Что тестирует |
|
||||
|---|---|---|---|
|
||||
| **Разработка** | Разработчик, локальный ПК | pytest (`just host::hil-*`) | Отдельные BSP модули через HIL ELF + UART |
|
||||
| **Производство / Сервис** | Оператор, сервер | ??? | Плата целиком через firmware_test + USB CDC |
|
||||
|
||||
Это два **принципиально разных** use case с разными требованиями к UX,
|
||||
надёжности и изоляции. Смешивать их в одном pytest-прогоне нельзя.
|
||||
|
||||
---
|
||||
|
||||
### Почему "просто pytest" недостаточно для производства
|
||||
|
||||
Производственный прогон отличается от HIL тестов разработчика по нескольким осям:
|
||||
|
||||
**Оператор — не разработчик.** Он не читает pytest output в терминале.
|
||||
Ему нужно видеть: какой тест сейчас идёт, прошёл или нет, что делать дальше
|
||||
(вставить карту, посмотреть на дисплей, нажать кнопку).
|
||||
|
||||
**Последовательность фиксирована.** Прогон всегда идёт по реестру:
|
||||
flash → READY → run_all → summary → provisioning. Никакого выбора тестов.
|
||||
|
||||
**Результат — не лог, а запись в БД.** `chip_uid` + результат + версия прошивки
|
||||
должны сохраняться. pytest ничего не знает о БД.
|
||||
|
||||
**Интерактивные тесты** (display) требуют управляемого диалога с оператором,
|
||||
а не `input()` в терминале с флагом `-s`.
|
||||
|
||||
---
|
||||
|
||||
### Варианты хостового ПО — анализ
|
||||
|
||||
#### Вариант A: pytest + плагины + HTML отчёт
|
||||
|
||||
Обернуть весь прогон в pytest, добавить `pytest-html` для отчётов,
|
||||
интерактивные тесты через `@pytest.mark.interactive` с `-s`.
|
||||
|
||||
**Плюсы:** минимум нового кода, знакомый инструмент.
|
||||
|
||||
**Минусы:** оператор смотрит в терминал; интерактивность через `/dev/tty` хрупкая;
|
||||
provisioning (запись в БД) — костыль в фикстуре; нет живого статуса
|
||||
"тест N из M идёт X секунд". Для производства неприемлемо.
|
||||
|
||||
#### Вариант B: TUI — Python + Textual / Rich
|
||||
|
||||
Самостоятельное Python-приложение с текстовым интерфейсом.
|
||||
Управляет всем: flash через spsdk, M5StampPLC, USB CDC, provisioning, БД.
|
||||
pytest не используется как runner — только как библиотека для assert-логики
|
||||
(или вообще не используется).
|
||||
|
||||
**Плюсы:** полный контроль над UX; живой прогресс; чёткие диалоги оператора;
|
||||
нативная запись в БД; изолировано от HIL тестов разработчика полностью.
|
||||
|
||||
**Минусы:** значительный объём разработки хостового ПО;
|
||||
нужен отдельный репозиторий или директория `tools/production/`.
|
||||
|
||||
#### Вариант C: pytest как backend, TUI/GUI как frontend
|
||||
|
||||
pytest запускается программно через `pytest.main()` или subprocess,
|
||||
результаты передаются через JSON reporter (`pytest-json-report`) в
|
||||
отдельное GUI/TUI приложение которое их отображает.
|
||||
|
||||
**Плюсы:** переиспользуем pytest инфраструктуру (фикстуры, параллелизм).
|
||||
|
||||
**Минусы:** архитектурно сложно; интерактивные тесты всё равно требуют
|
||||
кастомного канала; два процесса вместо одного.
|
||||
|
||||
#### Вариант D: pytest только для разработки, отдельный runner для производства
|
||||
|
||||
**Разработка:** pytest (`just host::hil-*`) — тесты отдельных BSP модулей
|
||||
через HIL ELF. Остаётся как есть.
|
||||
|
||||
**Производство:** отдельное Python-приложение (`tools/production/run.py`)
|
||||
без pytest. Использует те же низкоуровневые библиотеки
|
||||
(`pyserial`, `spsdk`, M5 агент) но со своим runner-ом и TUI.
|
||||
|
||||
Тест-логика на стороне прошивки (в `test_runner.c`) — единственная
|
||||
точка истины. Хостовое ПО только отправляет команды и интерпретирует события.
|
||||
|
||||
**Это рекомендуемый вариант** — наиболее чистое разделение.
|
||||
|
||||
---
|
||||
|
||||
### Рекомендуемая архитектура (Вариант D, детально)
|
||||
```
|
||||
tools/
|
||||
├── hil/ # СУЩЕСТВУЮЩИЙ — разработчик, локальный ПК
|
||||
│ ├── conftest.py # pytest фикстуры
|
||||
│ ├── 01_test_uart.py # HIL тесты отдельных BSP модулей
|
||||
│ ├── 02_test_opto.py
|
||||
│ └── ...
|
||||
│
|
||||
└── production/ # НОВЫЙ — производство / сервис
|
||||
├── pyproject.toml # отдельное окружение uv
|
||||
├── run.py # точка входа: python run.py
|
||||
├── runner/
|
||||
│ ├── flasher.py # spsdk: flash firmware_test.elf
|
||||
│ ├── cdc_client.py # USB CDC: send cmd, recv events
|
||||
│ ├── m5_client.py # переиспользовать tools/hil/m5/
|
||||
│ ├── test_sequence.py # порядок: flash→READY→run_all→provision
|
||||
│ └── db.py # запись chip_uid + результатов
|
||||
└── ui/
|
||||
├── tui.py # Textual TUI: живой прогресс + диалоги
|
||||
└── report.py # HTML / JSON отчёт после прогона
|
||||
```
|
||||
|
||||
**Поток производственного прогона:**
|
||||
```
|
||||
Оператор запускает: python tools/production/run.py
|
||||
│
|
||||
├── flasher.py: spsdk → flash firmware_test.elf → Reset
|
||||
├── cdc_client: ждёт {"type":"session_start",...}
|
||||
├── TUI: показывает "Подключение... ОК"
|
||||
│
|
||||
├── cdc_client: → {"type":"cmd","cmd":"run_all"}
|
||||
│
|
||||
├── [цикл событий]
|
||||
│ ├── {"type":"test_begin","id":"sdram"} → TUI: "SDRAM... ⏳"
|
||||
│ ├── {"type":"test_result","id":"sdram","status":"pass"} → TUI: "SDRAM ✅ 47ms"
|
||||
│ ├── {"type":"confirm_request","id":"display_red"} →
|
||||
│ │ TUI: диалог оператора "Экран красный? [PASS/FAIL]"
|
||||
│ │ cdc_client: → {"type":"confirm","id":"display_red","confirmed":true}
|
||||
│ └── ...
|
||||
│
|
||||
├── {"type":"summary","overall":"pass"} → TUI: финальный результат
|
||||
├── {"type":"provision_ready","chip_uid":"..."} →
|
||||
│ db.py: записать в БД
|
||||
│ cdc_client: → {"type":"cmd","cmd":"provision_ack",...}
|
||||
└── TUI: "Плата принята ✅ | UID: A3F2..." | печать этикетки?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Изоляция от HIL тестов разработчика
|
||||
|
||||
| Аспект | HIL тесты (разработка) | Production runner |
|
||||
|---|---|---|
|
||||
| Инструмент | pytest | Самостоятельный Python runner |
|
||||
| Директория | `tools/hil/` | `tools/production/` |
|
||||
| Окружение uv | `tools/hil/pyproject.toml` | `tools/production/pyproject.toml` |
|
||||
| Just рецепты | `just host::hil-*` | `just host::production-run` |
|
||||
| Прошивка | HIL ELF в RAM (pyOCD) | firmware_test в Flash (spsdk) |
|
||||
| Транспорт | UART CLI (pyserial) | USB CDC JSON-lines |
|
||||
| UX | Терминал / pytest output | TUI (Textual) |
|
||||
| БД | Нет | Да |
|
||||
| M5StampPLC | Да (реле для сигналов) | Да (те же реле) |
|
||||
| CI | Да (автоматический) | Нет |
|
||||
|
||||
Общий код (M5 агент, низкоуровневый spsdk wrapper) можно вынести в
|
||||
`tools/shared/` и подключать как локальный пакет в обоих `pyproject.toml`.
|
||||
|
||||
---
|
||||
|
||||
### Открытые вопросы для обсуждения
|
||||
|
||||
- [ ] **TUI библиотека:** Textual (современный, богатый) или Rich (проще, достаточно)?
|
||||
Или вообще минималистичный вывод без TUI фреймворка на первой итерации?
|
||||
- [ ] **БД:** SQLite локально на сервере или REST API на внешний сервис?
|
||||
- [ ] **Этикетка:** нужна ли автоматическая печать после provisioning?
|
||||
- [ ] **Несколько стендов:** один сервер управляет несколькими платами параллельно
|
||||
или всегда одна плата?
|
||||
- [ ] **Первая итерация:** допустимо ли начать с простого `run.py` без TUI
|
||||
(plain print + input) и добавить TUI позже?
|
||||
|
|
@ -1,244 +1,162 @@
|
|||
# firmware_test — Architecture
|
||||
# firmware_test
|
||||
|
||||
> Target: NXP IMXRT1052CVJ5B
|
||||
> Версия документа: 0.1
|
||||
> Статус: draft
|
||||
Bare-metal прошивка входного контроля платы **MIMXRT1052CVJ5B**.
|
||||
Загружается через USB ROM (SDP) в Flash. Запускается автономно при включении.
|
||||
|
||||
---
|
||||
|
||||
## 1. Назначение
|
||||
## Канал связи
|
||||
|
||||
`firmware_test` — входная тестовая прошивка для проверки работоспособности платы при производстве и во время разработки. Запускается напрямую через BootROM (USB Serial Download), без предварительной прошивки загрузчика. После успешного прохождения всех тестов инициирует фазу провижининга (привязка Chip UID к версиям ПО).
|
||||
|
||||
---
|
||||
|
||||
## 2. Workflow прошивки платы
|
||||
Единственный транспорт: **USB CDC ACM** (USB1 / EHCI0, разъём J2).
|
||||
UART (MCU-Link VCOM) не используется — только в HIL ELF прошивках.
|
||||
|
||||
```bash
|
||||
BootROM (USB Serial Download, встроен в IMXRT1052)
|
||||
↓
|
||||
firmware_test (залит напрямую)
|
||||
↓ [тесты прошли, provisioning выполнен]
|
||||
Флашим: Bootloader + App
|
||||
↓
|
||||
Ждём heartbeat Bootloader → App
|
||||
↓
|
||||
Плата принята
|
||||
Хост (pytest / minicom / pyserial)
|
||||
└── USB CDC ACM (J2)
|
||||
└── firmware_test
|
||||
└── cli.c → dispatch → обработчик команды
|
||||
```
|
||||
|
||||
Вариант с предварительной заливкой загрузчика не используется — BootROM является надёжным и всегда доступным recovery-path, не зависящим от состояния Flash.
|
||||
|
||||
---
|
||||
|
||||
## 3. Высокоуровневая архитектура
|
||||
## Протокол
|
||||
|
||||
**JSON-lines**: каждая строка завершается `\n`.
|
||||
|
||||
| Направление | Формат |
|
||||
|---|---|
|
||||
| Запрос | `{"cmd":"NAME"}\n` |
|
||||
| Успех | `{"ok":true,...}\n` |
|
||||
| Ошибка | `{"ok":false,"error":"CODE"}\n` |
|
||||
|
||||
Ограничения:
|
||||
|
||||
- Максимальная длина строки: **128 байт** включая `\n` (`CLI_LINE_BUF_SIZE`)
|
||||
- Терминатор: `\n` (LF, `0x0A`). `\r\n` (CR+LF) тоже принимается
|
||||
|
||||
---
|
||||
|
||||
## Команды
|
||||
|
||||
| Команда | Запрос | Успешный ответ | Описание |
|
||||
|---|---|---|---|
|
||||
| `PING` | `{"cmd":"PING"}` | `{"ok":true,"result":"PONG"}` | Проверка связи |
|
||||
| `SDRAM_TEST` | `{"cmd":"SDRAM_TEST"}` | `{"ok":true,"time_ms":N}` | Быстрый тест SDRAM (≤100 мс) |
|
||||
| `SDRAM_TEST_FULL` | `{"cmd":"SDRAM_TEST_FULL"}` | `{"ok":true,"time_ms":N}` | Полный тест SDRAM (~30–60 с) |
|
||||
|
||||
Коды ошибок:
|
||||
|
||||
| Код | Причина |
|
||||
|---|---|
|
||||
| `UNKNOWN_CMD` | Команда не найдена в таблице |
|
||||
| `PARSE_ERR` | Не найдено поле `"cmd"` в JSON |
|
||||
| `LINE_TOO_LONG` | Строка превысила `CLI_LINE_BUF_SIZE` |
|
||||
| `SDRAM_FAIL` | Тест SDRAM выявил ошибку чтения/записи |
|
||||
|
||||
---
|
||||
|
||||
## Последовательность старта
|
||||
|
||||
```bash
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ firmware_test │
|
||||
│ │
|
||||
│ USB CDC ──► Protocol ──► Test runner ──► Local UI │
|
||||
│ (JSON-lines) (sequencer) (LED+disp) │
|
||||
│ │
|
||||
│ ┌─────────────────────┐ ┌──────────────────────┐ │
|
||||
│ │ self-tests │ │ HIL tests │ │
|
||||
│ │ SDRAM QSPI uSD │ │ CAN UART Opto-in │ │
|
||||
│ │ RTC Display IR │ │ UART ISO IR burst │ │
|
||||
│ └─────────────────────┘ └──────────────────────┘ │
|
||||
│ │
|
||||
│ HAL / BSP │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
board_hw_init() тактирование, MPU, кэш, пины
|
||||
bsp_tick_init() SysTick 1 мс
|
||||
bsp_led_init() оба LED выключены
|
||||
bsp_usb_cdc_init() PHY + USB стек + NVIC
|
||||
|
||||
[ожидание хоста] LED_HEARTBEAT мигает 200 мс
|
||||
bsp_usb_cdc_is_ready() == true
|
||||
|
||||
LED_APP on
|
||||
→ {"ok":true,"result":"READY"} сигнал готовности хосту
|
||||
|
||||
[главный цикл]
|
||||
bsp_usb_cdc_poll()
|
||||
cli_process()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Компоненты
|
||||
|
||||
### 4.1 USB CDC
|
||||
|
||||
Единственный канал связи с внешним миром. Представляется хосту как виртуальный COM-порт. Инициализируется первым, до запуска тестов. При старте ожидает подключения хоста с таймаутом — если хост не подключился, тесты запускаются автономно.
|
||||
|
||||
### 4.2 Protocol
|
||||
|
||||
Протокол — **JSON-lines**: каждое сообщение является отдельным JSON-объектом, завершённым символом `\n`. Библиотека: cJSON из NXP SDK.
|
||||
|
||||
Направление **хост → плата** (команды):
|
||||
|
||||
```json
|
||||
{"type":"cmd","cmd":"run_all"}
|
||||
{"type":"cmd","cmd":"run","id":"sdram"}
|
||||
{"type":"confirm","id":"display","confirmed":true}
|
||||
```
|
||||
|
||||
Направление **плата → хост** (события):
|
||||
|
||||
```json
|
||||
{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
|
||||
{"type":"test_begin","id":"sdram","name":"SDRAM 32MB","critical":true}
|
||||
{"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":"32MB R/W OK"}
|
||||
{"type":"confirm_request","id":"display","timeout_ms":15000}
|
||||
{"type":"abort","reason":"critical_fail","id":"usd"}
|
||||
{"type":"summary","passed":7,"failed":0,"skipped":2,"aborted":false,"overall":"pass"}
|
||||
{"type":"provision_ready","chip_uid":"A3F2C1B400E70012"}
|
||||
{"type":"provision_ack","fw":"1.0.0","bootloader":"1.0.0","recorded":true}
|
||||
```
|
||||
|
||||
> **Примечание:** `uptime_ms` вместо Unix timestamp — RTC может быть не инициализирован на новой плате. Хост приклеивает реальное время самостоятельно.
|
||||
|
||||
### 4.3 Test runner
|
||||
|
||||
Центральный компонент. Хранит реестр тест-модулей, управляет порядком запуска, обрабатывает критические сбои, формирует `summary`.
|
||||
|
||||
**Порядок выполнения:**
|
||||
|
||||
1. Self-tests в порядке реестра
|
||||
2. Проверка критических сбоев — если есть, HIL не запускается
|
||||
3. HIL tests (только если Firefly подключён и self-tests прошли)
|
||||
4. Summary report
|
||||
5. Provisioning (только при `overall == pass`)
|
||||
|
||||
**Интерфейс тест-модуля (`test_module.h`):**
|
||||
|
||||
```cpp
|
||||
typedef enum {
|
||||
TEST_STATUS_PASS = 0,
|
||||
TEST_STATUS_FAIL,
|
||||
TEST_STATUS_SKIP,
|
||||
} test_status_t;
|
||||
|
||||
typedef struct {
|
||||
test_status_t status;
|
||||
uint32_t duration_ms;
|
||||
char detail[96]; /* диагностическая строка, опционально */
|
||||
} test_result_t;
|
||||
|
||||
typedef struct {
|
||||
const char *id; /* "sdram", "qspi", "can" — ключ в JSON */
|
||||
const char *name; /* "SDRAM 32MB" — для display/лога */
|
||||
bool critical; /* abort HIL если FAIL */
|
||||
bool requires_hil; /* пропустить если Firefly не готов */
|
||||
void (*init)(void);
|
||||
test_result_t (*run)(void);
|
||||
void (*deinit)(void);
|
||||
} test_module_t;
|
||||
```
|
||||
|
||||
### 4.4 Local UI
|
||||
|
||||
Отображает текущее состояние тестирования на светодиодах и дисплее. Получает события от Test runner. Дисплей при этом является частью тест-процесса (display_test).
|
||||
|
||||
**LED-паттерны:**
|
||||
|
||||
| Состояние | LED1 | LED2 |
|
||||
|------------------------|-------------|-------------|
|
||||
| Тест выполняется | мигает | выкл |
|
||||
| Все тесты PASS | вкл | выкл |
|
||||
| Есть FAIL | выкл | вкл |
|
||||
| Ожидание подтверждения | оба мигают | |
|
||||
|
||||
---
|
||||
|
||||
## 5. Тест-модули
|
||||
|
||||
### 5.1 Self-tests
|
||||
|
||||
| ID | Название | Critical | Описание |
|
||||
|------------|------------------|----------|--------------------------------------------------|
|
||||
| `sdram` | SDRAM 32MB | ✅ | Write/read паттерны по всему объёму |
|
||||
| `qspi` | QSPI Flash | ✅ | JEDEC ID + запись/чтение тестового сектора |
|
||||
| `usd` | uSD (SDIO) | ✅ | Mount + R/W тестового файла (SKIP если нет карты)|
|
||||
| `rtc` | RTC BM8563 | — | I2C presence, set/get time |
|
||||
| `display` | Display RGB888 | — | R/G/B/W заливки, подтверждение оператором |
|
||||
| `ir` | IR receiver | — | GPIO idle state HIGH, peripheral init |
|
||||
|
||||
**Display test — логика подтверждения:**
|
||||
|
||||
- Плата посылает `confirm_request` с `timeout_ms: 15000`
|
||||
- Оператор нажимает **одну** кнопку: PASS или FAIL
|
||||
- Если кнопка не нажата за 15 секунд — статус `SKIP` (ответственность на операторе)
|
||||
- Одновременно проверяются обе кнопки — это полноценный тест кнопок
|
||||
|
||||
### 5.2 HIL tests (требуют Firefly AIO-3588Q)
|
||||
|
||||
| ID | Название | Стенд | Описание |
|
||||
|--------------|------------------|------------------------------|----------------------------------------|
|
||||
| `can` | CAN | Firefly CAN | Обмен фреймами, full-duplex |
|
||||
| `uart_ttl` | UART TTL | Firefly UART | Echo паттерн |
|
||||
| `uart_iso` | UART ISO +24V | Firefly UART + интерф. плата | Только RX, Firefly посылает |
|
||||
| `opto` | Opto-in +24V | Firefly GPIO + интерф. плата | Все каналы, Firefly дёргает GPIO |
|
||||
| `ir_hil` | IR burst | Firefly GPIO + IR LED | Приём burst 38 кГц, факт прерывания |
|
||||
|
||||
---
|
||||
|
||||
## 6. Provisioning
|
||||
|
||||
Выполняется после `summary: overall == pass`. Не является тестом — это отдельный этап жизненного цикла платы.
|
||||
|
||||
**Источник UID:** регистры OCOTP (One-Time Programmable fuses), 64-bit Chip UID. Читается через HAL.
|
||||
|
||||
**Интерфейс (`provisioning.h`):**
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
char chip_uid[17]; /* 64-bit UID как hex-строка, null-terminated */
|
||||
char fw_version[16];
|
||||
char bootloader_version[16];
|
||||
bool provisioned;
|
||||
} provision_info_t;
|
||||
|
||||
/* вызывается только при overall == PASS */
|
||||
void provisioning_run(provision_info_t *out);
|
||||
```
|
||||
|
||||
**Поток:**
|
||||
## Структура файлов
|
||||
|
||||
```bash
|
||||
плата посылает provision_ready + chip_uid
|
||||
↓
|
||||
хост записывает в БД: uid ↔ fw_version ↔ bootloader_version
|
||||
↓
|
||||
хост посылает provision_ack
|
||||
↓
|
||||
плата устанавливает provisioned = true
|
||||
```
|
||||
|
||||
Вся логика на стороне хоста (запись в БД, генерация сертификата, привязка партии). Прошивка только читает UID и ждёт подтверждения.
|
||||
|
||||
---
|
||||
|
||||
## 7. Реестр тестов
|
||||
|
||||
```c
|
||||
/* test_registry.c */
|
||||
static const test_module_t *tests[] = {
|
||||
&test_sdram, /* critical */
|
||||
&test_qspi, /* critical */
|
||||
&test_usd, /* critical, SKIP если нет карты */
|
||||
&test_rtc,
|
||||
&test_display, /* operator confirm */
|
||||
&test_ir, /* self-test уровень */
|
||||
&test_can, /* requires_hil */
|
||||
&test_uart_ttl, /* requires_hil */
|
||||
&test_uart_iso, /* requires_hil */
|
||||
&test_opto, /* requires_hil */
|
||||
&test_ir_hil, /* requires_hil, SKIP если нет IR на стенде */
|
||||
};
|
||||
firmware/test/
|
||||
├── CMakeLists.txt
|
||||
├── README.md ← этот файл
|
||||
└── src/
|
||||
├── main.c # точка входа, init, main loop
|
||||
├── cli.h # публичный API CLI
|
||||
├── cli.c # буферизация RX, парсинг "cmd", dispatch
|
||||
└── tests/
|
||||
├── test_sdram.c # SDRAM_TEST / SDRAM_TEST_FULL [TODO]
|
||||
├── test_sdram.h
|
||||
├── test_led.c # LED_ON / LED_OFF [TODO]
|
||||
├── test_led.h
|
||||
├── test_button.c # BUTTON_READ [TODO]
|
||||
└── test_button.h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Статусы тестов
|
||||
## Добавление новой команды
|
||||
|
||||
| Статус | Значение |
|
||||
|--------|-------------------------------------------------------|
|
||||
| `PASS` | Тест прошёл успешно |
|
||||
| `FAIL` | Тест провален, в `detail` диагностическая информация |
|
||||
| `SKIP` | Тест пропущен (нет карты, нет Firefly, таймаут оператора) |
|
||||
1. В `cli.c` объявить обработчик: `static void cmd_foo(void);`
|
||||
2. Добавить строку в таблицу `k_cmds[]`: `{ "FOO", cmd_foo }`
|
||||
3. Реализовать обработчик в секции `Command handlers`
|
||||
4. Добавить тест в `tests/host/cli/test_cli.c`
|
||||
5. Обновить таблицу команд в этом README
|
||||
|
||||
---
|
||||
|
||||
## 9. Открытые вопросы
|
||||
## Сборка и прошивка
|
||||
|
||||
- [ ] Формат `detail` при FAIL для каждого теста (договориться между разработчиками)
|
||||
- [ ] Handshake-протокол между firmware_test и Firefly (как плата узнаёт о готовности стенда)
|
||||
- [ ] Полная схема интерфейсной платы для Firefly (оптовходы, IR LED, уровни +24V)
|
||||
- [ ] GUI на сервере: формат отображения `summary` и хранение истории плат
|
||||
```bash
|
||||
# devcontainer
|
||||
just build::build-firmware-test-debug
|
||||
just build::hab-firmware-test-debug
|
||||
|
||||
# хост (плата в SDP-режиме: BOOT_MOD_1 → 3V3 → Reset)
|
||||
just host::flash-test-debug
|
||||
|
||||
# вернуть в нормальный режим
|
||||
# BOOT_MOD_1 → GND → Reset
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Тестирование
|
||||
|
||||
```bash
|
||||
# Host unit-тесты CLI парсера (devcontainer, без железа)
|
||||
just build::test-host # → test_cli
|
||||
|
||||
# HIL тест USB CDC (хост, требует подключённой платы)
|
||||
just host::hil-usb-cdc
|
||||
|
||||
# HIL тест SDRAM (хост, firmware_test прошит в Flash) ????????
|
||||
just host::hil-sdram # быстрый
|
||||
just host::hil-sdram-full # полный (~60 с)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
```bash
|
||||
HIL_USB_CDC_PORT= # порт USB CDC таргета, например /dev/ttyACM1
|
||||
HIL_USB_CDC_BAUD= # 115200
|
||||
HIL_USB_CDC_TIMEOUT= # 5.0
|
||||
```
|
||||
|
||||
Шаблон: `.env.example`. Актуальные значения: `.env` (не коммитится).
|
||||
|
||||
---
|
||||
|
||||
## Известные ограничения
|
||||
|
||||
- **Один пакет за цикл**: `cli_process()` читает один USB bulk-пакет за вызов
|
||||
главного цикла. При потоке команд без задержки буфер может не успеть
|
||||
обработаться — добавить задержку на стороне хоста между командами (≥10 мс).
|
||||
- **TX неблокирующий**: если TX занят — ответ теряется. Хост должен ждать
|
||||
предыдущий ответ перед отправкой следующей команды.
|
||||
- **Нет персистентного состояния**: при сбросе питания все результаты теряются.
|
||||
pytest должен повторно подключаться и дожидаться `READY`.
|
||||
|
|
|
|||
244
firmware/test/TODO.md
Normal file
244
firmware/test/TODO.md
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
# firmware_test — Architecture
|
||||
|
||||
> Target: NXP IMXRT1052CVJ5B
|
||||
> Версия документа: 0.1
|
||||
> Статус: draft
|
||||
|
||||
---
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
`firmware_test` — входная тестовая прошивка для проверки работоспособности платы при производстве и во время разработки. Запускается напрямую через BootROM (USB Serial Download), без предварительной прошивки загрузчика. После успешного прохождения всех тестов инициирует фазу провижининга (привязка Chip UID к версиям ПО).
|
||||
|
||||
---
|
||||
|
||||
## 2. Workflow прошивки платы
|
||||
|
||||
```bash
|
||||
BootROM (USB Serial Download, встроен в IMXRT1052)
|
||||
↓
|
||||
firmware_test (залит напрямую)
|
||||
↓ [тесты прошли, provisioning выполнен]
|
||||
Флашим: Bootloader + App
|
||||
↓
|
||||
Ждём heartbeat Bootloader → App
|
||||
↓
|
||||
Плата принята
|
||||
```
|
||||
|
||||
Вариант с предварительной заливкой загрузчика не используется — BootROM является надёжным и всегда доступным recovery-path, не зависящим от состояния Flash.
|
||||
|
||||
---
|
||||
|
||||
## 3. Высокоуровневая архитектура
|
||||
|
||||
```bash
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ firmware_test │
|
||||
│ │
|
||||
│ USB CDC ──► Protocol ──► Test runner ──► Local UI │
|
||||
│ (JSON-lines) (sequencer) (LED+disp) │
|
||||
│ │
|
||||
│ ┌─────────────────────┐ ┌──────────────────────┐ │
|
||||
│ │ self-tests │ │ HIL tests │ │
|
||||
│ │ SDRAM QSPI uSD │ │ CAN UART Opto-in │ │
|
||||
│ │ RTC Display IR │ │ UART ISO IR burst │ │
|
||||
│ └─────────────────────┘ └──────────────────────┘ │
|
||||
│ │
|
||||
│ HAL / BSP │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Компоненты
|
||||
|
||||
### 4.1 USB CDC
|
||||
|
||||
Единственный канал связи с внешним миром. Представляется хосту как виртуальный COM-порт. Инициализируется первым, до запуска тестов. При старте ожидает подключения хоста с таймаутом — если хост не подключился, тесты запускаются автономно.
|
||||
|
||||
### 4.2 Protocol
|
||||
|
||||
Протокол — **JSON-lines**: каждое сообщение является отдельным JSON-объектом, завершённым символом `\n`. Библиотека: cJSON из NXP SDK.
|
||||
|
||||
Направление **хост → плата** (команды):
|
||||
|
||||
```json
|
||||
{"type":"cmd","cmd":"run_all"}
|
||||
{"type":"cmd","cmd":"run","id":"sdram"}
|
||||
{"type":"confirm","id":"display","confirmed":true}
|
||||
```
|
||||
|
||||
Направление **плата → хост** (события):
|
||||
|
||||
```json
|
||||
{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
|
||||
{"type":"test_begin","id":"sdram","name":"SDRAM 32MB","critical":true}
|
||||
{"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":"32MB R/W OK"}
|
||||
{"type":"confirm_request","id":"display","timeout_ms":15000}
|
||||
{"type":"abort","reason":"critical_fail","id":"usd"}
|
||||
{"type":"summary","passed":7,"failed":0,"skipped":2,"aborted":false,"overall":"pass"}
|
||||
{"type":"provision_ready","chip_uid":"A3F2C1B400E70012"}
|
||||
{"type":"provision_ack","fw":"1.0.0","bootloader":"1.0.0","recorded":true}
|
||||
```
|
||||
|
||||
> **Примечание:** `uptime_ms` вместо Unix timestamp — RTC может быть не инициализирован на новой плате. Хост приклеивает реальное время самостоятельно.
|
||||
|
||||
### 4.3 Test runner
|
||||
|
||||
Центральный компонент. Хранит реестр тест-модулей, управляет порядком запуска, обрабатывает критические сбои, формирует `summary`.
|
||||
|
||||
**Порядок выполнения:**
|
||||
|
||||
1. Self-tests в порядке реестра
|
||||
2. Проверка критических сбоев — если есть, HIL не запускается
|
||||
3. HIL tests (только если Firefly подключён и self-tests прошли)
|
||||
4. Summary report
|
||||
5. Provisioning (только при `overall == pass`)
|
||||
|
||||
**Интерфейс тест-модуля (`test_module.h`):**
|
||||
|
||||
```cpp
|
||||
typedef enum {
|
||||
TEST_STATUS_PASS = 0,
|
||||
TEST_STATUS_FAIL,
|
||||
TEST_STATUS_SKIP,
|
||||
} test_status_t;
|
||||
|
||||
typedef struct {
|
||||
test_status_t status;
|
||||
uint32_t duration_ms;
|
||||
char detail[96]; /* диагностическая строка, опционально */
|
||||
} test_result_t;
|
||||
|
||||
typedef struct {
|
||||
const char *id; /* "sdram", "qspi", "can" — ключ в JSON */
|
||||
const char *name; /* "SDRAM 32MB" — для display/лога */
|
||||
bool critical; /* abort HIL если FAIL */
|
||||
bool requires_hil; /* пропустить если Firefly не готов */
|
||||
void (*init)(void);
|
||||
test_result_t (*run)(void);
|
||||
void (*deinit)(void);
|
||||
} test_module_t;
|
||||
```
|
||||
|
||||
### 4.4 Local UI
|
||||
|
||||
Отображает текущее состояние тестирования на светодиодах и дисплее. Получает события от Test runner. Дисплей при этом является частью тест-процесса (display_test).
|
||||
|
||||
**LED-паттерны:**
|
||||
|
||||
| Состояние | LED1 | LED2 |
|
||||
|------------------------|-------------|-------------|
|
||||
| Тест выполняется | мигает | выкл |
|
||||
| Все тесты PASS | вкл | выкл |
|
||||
| Есть FAIL | выкл | вкл |
|
||||
| Ожидание подтверждения | оба мигают | |
|
||||
|
||||
---
|
||||
|
||||
## 5. Тест-модули
|
||||
|
||||
### 5.1 Self-tests
|
||||
|
||||
| ID | Название | Critical | Описание |
|
||||
|------------|------------------|----------|--------------------------------------------------|
|
||||
| `sdram` | SDRAM 32MB | ✅ | Write/read паттерны по всему объёму |
|
||||
| `qspi` | QSPI Flash | ✅ | JEDEC ID + запись/чтение тестового сектора |
|
||||
| `usd` | uSD (SDIO) | ✅ | Mount + R/W тестового файла (SKIP если нет карты)|
|
||||
| `rtc` | RTC BM8563 | — | I2C presence, set/get time |
|
||||
| `display` | Display RGB888 | — | R/G/B/W заливки, подтверждение оператором |
|
||||
| `ir` | IR receiver | — | GPIO idle state HIGH, peripheral init |
|
||||
|
||||
**Display test — логика подтверждения:**
|
||||
|
||||
- Плата посылает `confirm_request` с `timeout_ms: 15000`
|
||||
- Оператор нажимает **одну** кнопку: PASS или FAIL
|
||||
- Если кнопка не нажата за 15 секунд — статус `SKIP` (ответственность на операторе)
|
||||
- Одновременно проверяются обе кнопки — это полноценный тест кнопок
|
||||
|
||||
### 5.2 HIL tests (требуют Firefly AIO-3588Q)
|
||||
|
||||
| ID | Название | Стенд | Описание |
|
||||
|--------------|------------------|------------------------------|----------------------------------------|
|
||||
| `can` | CAN | Firefly CAN | Обмен фреймами, full-duplex |
|
||||
| `uart_ttl` | UART TTL | Firefly UART | Echo паттерн |
|
||||
| `uart_iso` | UART ISO +24V | Firefly UART + интерф. плата | Только RX, Firefly посылает |
|
||||
| `opto` | Opto-in +24V | Firefly GPIO + интерф. плата | Все каналы, Firefly дёргает GPIO |
|
||||
| `ir_hil` | IR burst | Firefly GPIO + IR LED | Приём burst 38 кГц, факт прерывания |
|
||||
|
||||
---
|
||||
|
||||
## 6. Provisioning
|
||||
|
||||
Выполняется после `summary: overall == pass`. Не является тестом — это отдельный этап жизненного цикла платы.
|
||||
|
||||
**Источник UID:** регистры OCOTP (One-Time Programmable fuses), 64-bit Chip UID. Читается через HAL.
|
||||
|
||||
**Интерфейс (`provisioning.h`):**
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
char chip_uid[17]; /* 64-bit UID как hex-строка, null-terminated */
|
||||
char fw_version[16];
|
||||
char bootloader_version[16];
|
||||
bool provisioned;
|
||||
} provision_info_t;
|
||||
|
||||
/* вызывается только при overall == PASS */
|
||||
void provisioning_run(provision_info_t *out);
|
||||
```
|
||||
|
||||
**Поток:**
|
||||
|
||||
```bash
|
||||
плата посылает provision_ready + chip_uid
|
||||
↓
|
||||
хост записывает в БД: uid ↔ fw_version ↔ bootloader_version
|
||||
↓
|
||||
хост посылает provision_ack
|
||||
↓
|
||||
плата устанавливает provisioned = true
|
||||
```
|
||||
|
||||
Вся логика на стороне хоста (запись в БД, генерация сертификата, привязка партии). Прошивка только читает UID и ждёт подтверждения.
|
||||
|
||||
---
|
||||
|
||||
## 7. Реестр тестов
|
||||
|
||||
```c
|
||||
/* test_registry.c */
|
||||
static const test_module_t *tests[] = {
|
||||
&test_sdram, /* critical */
|
||||
&test_qspi, /* critical */
|
||||
&test_usd, /* critical, SKIP если нет карты */
|
||||
&test_rtc,
|
||||
&test_display, /* operator confirm */
|
||||
&test_ir, /* self-test уровень */
|
||||
&test_can, /* requires_hil */
|
||||
&test_uart_ttl, /* requires_hil */
|
||||
&test_uart_iso, /* requires_hil */
|
||||
&test_opto, /* requires_hil */
|
||||
&test_ir_hil, /* requires_hil, SKIP если нет IR на стенде */
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Статусы тестов
|
||||
|
||||
| Статус | Значение |
|
||||
|--------|-------------------------------------------------------|
|
||||
| `PASS` | Тест прошёл успешно |
|
||||
| `FAIL` | Тест провален, в `detail` диагностическая информация |
|
||||
| `SKIP` | Тест пропущен (нет карты, нет Firefly, таймаут оператора) |
|
||||
|
||||
---
|
||||
|
||||
## 9. Открытые вопросы
|
||||
|
||||
- [ ] Формат `detail` при FAIL для каждого теста (договориться между разработчиками)
|
||||
- [ ] Handshake-протокол между firmware_test и Firefly (как плата узнаёт о готовности стенда)
|
||||
- [ ] Полная схема интерфейсной платы для Firefly (оптовходы, IR LED, уровни +24V)
|
||||
- [ ] GUI на сервере: формат отображения `summary` и хранение истории плат
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1360" viewBox="0 0 680 1180">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
<style>
|
||||
text { font-family: system-ui, sans-serif; fill: #1a1a1a; }
|
||||
.th { font-size: 14px; font-weight: 500; }
|
||||
.ts { font-size: 12px; font-weight: 400; fill: #555; }
|
||||
.label-section { font-size: 11px; font-weight: 400; fill: #888; }
|
||||
|
||||
/* teal */
|
||||
.c-teal rect, .c-teal circle { fill: #E1F5EE; stroke: #0F6E56; }
|
||||
.c-teal .th { fill: #085041; }
|
||||
.c-teal .ts { fill: #0F6E56; }
|
||||
|
||||
/* purple */
|
||||
.c-purple rect { fill: #EEEDFE; stroke: #534AB7; }
|
||||
.c-purple .th { fill: #3C3489; }
|
||||
.c-purple .ts { fill: #534AB7; }
|
||||
|
||||
/* amber */
|
||||
.c-amber rect { fill: #FAEEDA; stroke: #854F0B; }
|
||||
.c-amber .th { fill: #633806; }
|
||||
.c-amber .ts { fill: #854F0B; }
|
||||
|
||||
/* gray */
|
||||
.c-gray rect { fill: #F1EFE8; stroke: #5F5E5A; }
|
||||
.c-gray .th { fill: #2C2C2A; }
|
||||
.c-gray .ts { fill: #5F5E5A; }
|
||||
|
||||
/* red */
|
||||
.c-red rect { fill: #FCEBEB; stroke: #A32D2D; }
|
||||
.c-red .th { fill: #501313; }
|
||||
.c-red .ts { fill: #A32D2D; }
|
||||
|
||||
.arr { stroke: #888; stroke-width: 1; fill: none; }
|
||||
.box-dashed { fill: none; stroke: #bbb; stroke-width: 0.8; stroke-dasharray: 5 3; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════
|
||||
DIAGRAM 1: High-level architecture
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
|
||||
<text x="340" y="30" text-anchor="middle" class="th" style="font-size:16px;fill:#1a1a1a;">firmware_test · high-level architecture</text>
|
||||
<text x="340" y="48" text-anchor="middle" class="ts">NXP IMXRT1052CVJ5B</text>
|
||||
|
||||
<rect x="20" y="60" width="640" height="440" rx="16" class="box-dashed"/>
|
||||
<text x="340" y="78" text-anchor="middle" class="label-section">firmware_test</text>
|
||||
|
||||
<!-- USB CDC -->
|
||||
<g class="c-teal">
|
||||
<rect x="40" y="88" width="160" height="50" rx="8" stroke-width="0.5"/>
|
||||
<text class="th" x="120" y="108" text-anchor="middle" dominant-baseline="central">USB CDC</text>
|
||||
<text class="ts" x="120" y="126" text-anchor="middle" dominant-baseline="central">virtual serial port</text>
|
||||
</g>
|
||||
<text class="ts" x="212" y="107" fill="#888">↔ Host PC</text>
|
||||
<text class="ts" x="212" y="123" fill="#888">(терминал / GUI)</text>
|
||||
|
||||
<line x1="120" y1="138" x2="120" y2="180" class="arr" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Protocol -->
|
||||
<g class="c-purple">
|
||||
<rect x="40" y="180" width="160" height="50" rx="8" stroke-width="0.5"/>
|
||||
<text class="th" x="120" y="200" text-anchor="middle" dominant-baseline="central">Protocol</text>
|
||||
<text class="ts" x="120" y="218" text-anchor="middle" dominant-baseline="central">JSON-lines · cJSON</text>
|
||||
</g>
|
||||
|
||||
<!-- Test runner -->
|
||||
<g class="c-purple">
|
||||
<rect x="250" y="180" width="190" height="50" rx="8" stroke-width="0.5"/>
|
||||
<text class="th" x="345" y="200" text-anchor="middle" dominant-baseline="central">Test runner</text>
|
||||
<text class="ts" x="345" y="218" text-anchor="middle" dominant-baseline="central">sequencer + registry</text>
|
||||
</g>
|
||||
|
||||
<!-- Local UI -->
|
||||
<g class="c-amber">
|
||||
<rect x="490" y="180" width="150" height="50" rx="8" stroke-width="0.5"/>
|
||||
<text class="th" x="565" y="200" text-anchor="middle" dominant-baseline="central">Local UI</text>
|
||||
<text class="ts" x="565" y="218" text-anchor="middle" dominant-baseline="central">LEDs + display</text>
|
||||
</g>
|
||||
|
||||
<line x1="200" y1="205" x2="250" y2="205" class="arr" marker-start="url(#arrow)" marker-end="url(#arrow)" stroke="#666"/>
|
||||
<line x1="440" y1="205" x2="490" y2="205" class="arr" marker-end="url(#arrow)" stroke="#666"/>
|
||||
|
||||
<path d="M345,230 L345,260 L180,260 L180,270" fill="none" stroke="#bbb" stroke-width="0.8" marker-end="url(#arrow)"/>
|
||||
<path d="M345,230 L345,260 L505,260 L505,270" fill="none" stroke="#bbb" stroke-width="0.8" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Self-tests region -->
|
||||
<rect x="30" y="270" width="300" height="150" rx="8" class="box-dashed"/>
|
||||
<text class="label-section" x="56" y="287">self-tests</text>
|
||||
|
||||
<g class="c-gray"><rect x="46" y="295" width="76" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="84" y="312" text-anchor="middle" dominant-baseline="central">SDRAM</text>
|
||||
<text class="ts" x="84" y="328" text-anchor="middle" dominant-baseline="central">32 MB</text></g>
|
||||
<g class="c-gray"><rect x="132" y="295" width="76" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="170" y="312" text-anchor="middle" dominant-baseline="central">QSPI</text>
|
||||
<text class="ts" x="170" y="328" text-anchor="middle" dominant-baseline="central">W25Q128</text></g>
|
||||
<g class="c-gray"><rect x="218" y="295" width="76" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="256" y="312" text-anchor="middle" dominant-baseline="central">uSD</text>
|
||||
<text class="ts" x="256" y="328" text-anchor="middle" dominant-baseline="central">SDIO</text></g>
|
||||
<g class="c-gray"><rect x="46" y="349" width="76" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="84" y="366" text-anchor="middle" dominant-baseline="central">RTC</text>
|
||||
<text class="ts" x="84" y="382" text-anchor="middle" dominant-baseline="central">BM8563</text></g>
|
||||
<g class="c-gray"><rect x="132" y="349" width="76" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="170" y="366" text-anchor="middle" dominant-baseline="central">Display</text>
|
||||
<text class="ts" x="170" y="382" text-anchor="middle" dominant-baseline="central">operator ✓</text></g>
|
||||
<g class="c-gray"><rect x="218" y="349" width="76" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="256" y="366" text-anchor="middle" dominant-baseline="central">IR</text>
|
||||
<text class="ts" x="256" y="382" text-anchor="middle" dominant-baseline="central">idle GPIO</text></g>
|
||||
|
||||
<!-- HIL tests region -->
|
||||
<rect x="350" y="270" width="290" height="150" rx="8" class="box-dashed"/>
|
||||
<text class="label-section" x="376" y="287">HIL tests · Firefly AIO-3588Q</text>
|
||||
|
||||
<g class="c-teal"><rect x="366" y="295" width="120" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="426" y="312" text-anchor="middle" dominant-baseline="central">CAN</text>
|
||||
<text class="ts" x="426" y="328" text-anchor="middle" dominant-baseline="central">frame exchange</text></g>
|
||||
<g class="c-teal"><rect x="496" y="295" width="120" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="556" y="312" text-anchor="middle" dominant-baseline="central">UART TTL</text>
|
||||
<text class="ts" x="556" y="328" text-anchor="middle" dominant-baseline="central">echo pattern</text></g>
|
||||
<g class="c-teal"><rect x="366" y="349" width="120" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="426" y="366" text-anchor="middle" dominant-baseline="central">UART ISO</text>
|
||||
<text class="ts" x="426" y="382" text-anchor="middle" dominant-baseline="central">+24V RX only</text></g>
|
||||
<g class="c-teal"><rect x="496" y="349" width="120" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="556" y="366" text-anchor="middle" dominant-baseline="central">Opto-in</text>
|
||||
<text class="ts" x="556" y="382" text-anchor="middle" dominant-baseline="central">+24V all ch.</text></g>
|
||||
|
||||
<!-- HAL / BSP -->
|
||||
<line x1="340" y1="420" x2="340" y2="448" class="arr" marker-end="url(#arrow)" stroke="#aaa"/>
|
||||
<g class="c-gray">
|
||||
<rect x="30" y="448" width="620" height="40" rx="8" stroke-width="0.5"/>
|
||||
<text class="th" x="340" y="468" text-anchor="middle" dominant-baseline="central">HAL / BSP</text>
|
||||
</g>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════
|
||||
DIAGRAM 2: Test runner flow
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
|
||||
<text x="340" y="540" text-anchor="middle" class="th" style="font-size:16px;fill:#1a1a1a;">firmware_test · test runner flow</text>
|
||||
|
||||
<!-- Boot -->
|
||||
<g class="c-gray">
|
||||
<rect x="240" y="558" width="200" height="44" rx="8" stroke-width="0.5"/>
|
||||
<text class="th" x="340" y="576" text-anchor="middle" dominant-baseline="central">Boot</text>
|
||||
<text class="ts" x="340" y="592" text-anchor="middle" dominant-baseline="central">clock, USB CDC, HAL init</text>
|
||||
</g>
|
||||
<line x1="340" y1="602" x2="340" y2="630" class="arr" marker-end="url(#arrow)" stroke="#888"/>
|
||||
|
||||
<!-- Wait for host -->
|
||||
<g class="c-purple">
|
||||
<rect x="240" y="630" width="200" height="44" rx="8" stroke-width="0.5"/>
|
||||
<text class="th" x="340" y="648" text-anchor="middle" dominant-baseline="central">Wait for host</text>
|
||||
<text class="ts" x="340" y="664" text-anchor="middle" dominant-baseline="central">USB connect or timeout</text>
|
||||
</g>
|
||||
<line x1="340" y1="674" x2="340" y2="698" class="arr" marker-end="url(#arrow)" stroke="#888"/>
|
||||
|
||||
<!-- Self-tests block -->
|
||||
<rect x="30" y="698" width="620" height="168" rx="10" class="box-dashed"/>
|
||||
<text class="label-section" x="56" y="714">self-tests</text>
|
||||
|
||||
<g class="c-red">
|
||||
<rect x="46" y="720" width="110" height="54" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="101" y="740" text-anchor="middle" dominant-baseline="central">SDRAM</text>
|
||||
<text class="ts" x="101" y="756" text-anchor="middle" dominant-baseline="central">critical</text>
|
||||
<text class="ts" x="101" y="768" text-anchor="middle" dominant-baseline="central">write/read pattern</text>
|
||||
</g>
|
||||
<g class="c-red">
|
||||
<rect x="166" y="720" width="110" height="54" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="221" y="740" text-anchor="middle" dominant-baseline="central">QSPI Flash</text>
|
||||
<text class="ts" x="221" y="756" text-anchor="middle" dominant-baseline="central">critical</text>
|
||||
<text class="ts" x="221" y="768" text-anchor="middle" dominant-baseline="central">JEDEC ID + R/W</text>
|
||||
</g>
|
||||
<g class="c-red">
|
||||
<rect x="286" y="720" width="110" height="54" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="341" y="740" text-anchor="middle" dominant-baseline="central">uSD</text>
|
||||
<text class="ts" x="341" y="756" text-anchor="middle" dominant-baseline="central">critical</text>
|
||||
<text class="ts" x="341" y="768" text-anchor="middle" dominant-baseline="central">mount + R/W</text>
|
||||
</g>
|
||||
<g class="c-gray">
|
||||
<rect x="406" y="720" width="110" height="54" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="461" y="740" text-anchor="middle" dominant-baseline="central">RTC</text>
|
||||
<text class="ts" x="461" y="756" text-anchor="middle" dominant-baseline="central">I2C presence</text>
|
||||
<text class="ts" x="461" y="768" text-anchor="middle" dominant-baseline="central">set / get</text>
|
||||
</g>
|
||||
<g class="c-amber">
|
||||
<rect x="526" y="720" width="110" height="54" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="581" y="740" text-anchor="middle" dominant-baseline="central">Display</text>
|
||||
<text class="ts" x="581" y="756" text-anchor="middle" dominant-baseline="central">R/G/B/W fill</text>
|
||||
<text class="ts" x="581" y="768" text-anchor="middle" dominant-baseline="central">btn confirm</text>
|
||||
</g>
|
||||
|
||||
<line x1="101" y1="774" x2="101" y2="794" class="arr" marker-end="url(#arrow)" stroke="#888"/>
|
||||
<line x1="221" y1="774" x2="221" y2="794" class="arr" marker-end="url(#arrow)" stroke="#888"/>
|
||||
<line x1="341" y1="774" x2="341" y2="794" class="arr" marker-end="url(#arrow)" stroke="#888"/>
|
||||
<line x1="461" y1="774" x2="461" y2="794" class="arr" marker-end="url(#arrow)" stroke="#888"/>
|
||||
<line x1="581" y1="774" x2="581" y2="794" class="arr" marker-end="url(#arrow)" stroke="#888"/>
|
||||
|
||||
<!-- Check critical failures -->
|
||||
<g class="c-gray">
|
||||
<rect x="46" y="794" width="590" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="341" y="812" text-anchor="middle" dominant-baseline="central">Check critical failures</text>
|
||||
<text class="ts" x="341" y="828" text-anchor="middle" dominant-baseline="central">SDRAM || QSPI || uSD FAIL → abort HIL, report immediately</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="866" x2="340" y2="890" class="arr" marker-end="url(#arrow)" stroke="#888"/>
|
||||
|
||||
<!-- HIL block -->
|
||||
<rect x="30" y="890" width="620" height="78" rx="10" class="box-dashed"/>
|
||||
<text class="label-section" x="56" y="906">HIL tests · requires Firefly</text>
|
||||
|
||||
<g class="c-teal">
|
||||
<rect x="46" y="912" width="100" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="96" y="930" text-anchor="middle" dominant-baseline="central">CAN</text>
|
||||
<text class="ts" x="96" y="946" text-anchor="middle" dominant-baseline="central">full-duplex</text>
|
||||
</g>
|
||||
<g class="c-teal">
|
||||
<rect x="156" y="912" width="100" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="206" y="930" text-anchor="middle" dominant-baseline="central">UART TTL</text>
|
||||
<text class="ts" x="206" y="946" text-anchor="middle" dominant-baseline="central">echo</text>
|
||||
</g>
|
||||
<g class="c-teal">
|
||||
<rect x="266" y="912" width="100" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="316" y="930" text-anchor="middle" dominant-baseline="central">UART ISO</text>
|
||||
<text class="ts" x="316" y="946" text-anchor="middle" dominant-baseline="central">RX only</text>
|
||||
</g>
|
||||
<g class="c-teal">
|
||||
<rect x="376" y="912" width="100" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="426" y="930" text-anchor="middle" dominant-baseline="central">Opto-in</text>
|
||||
<text class="ts" x="426" y="946" text-anchor="middle" dominant-baseline="central">all channels</text>
|
||||
</g>
|
||||
<g class="c-gray">
|
||||
<rect x="486" y="912" width="100" height="44" rx="6" stroke-width="0.5"/>
|
||||
<text class="th" x="536" y="930" text-anchor="middle" dominant-baseline="central">IR burst</text>
|
||||
<text class="ts" x="536" y="946" text-anchor="middle" dominant-baseline="central">38 kHz RX</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="968" x2="340" y2="996" class="arr" marker-end="url(#arrow)" stroke="#888"/>
|
||||
|
||||
<!-- Summary -->
|
||||
<g class="c-teal">
|
||||
<rect x="160" y="996" width="360" height="50" rx="8" stroke-width="0.5"/>
|
||||
<text class="th" x="340" y="1016" text-anchor="middle" dominant-baseline="central">Summary report</text>
|
||||
<text class="ts" x="340" y="1034" text-anchor="middle" dominant-baseline="central">JSON · pass/fail/skip per test · overall</text>
|
||||
</g>
|
||||
|
||||
<line x1="340" y1="1046" x2="340" y2="1074" class="arr" marker-end="url(#arrow)" stroke="#aaa" stroke-dasharray="4 3"/>
|
||||
|
||||
<!-- Provisioning -->
|
||||
<g class="c-purple">
|
||||
<rect x="160" y="1074" width="360" height="50" rx="8" stroke-width="0.5"/>
|
||||
<text class="th" x="340" y="1094" text-anchor="middle" dominant-baseline="central">Provisioning</text>
|
||||
<text class="ts" x="340" y="1112" text-anchor="middle" dominant-baseline="central">OCOTP chip_uid → host DB · only on pass</text>
|
||||
</g>
|
||||
|
||||
<text class="ts" x="536" y="1050" fill="#aaa">only if overall == pass</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="30" y="1140" width="260" height="30" rx="6" fill="none" stroke="#eee" stroke-width="0.5"/>
|
||||
<rect x="40" y="1151" width="12" height="8" rx="2" fill="#FCEBEB" stroke="#A32D2D" stroke-width="0.5"/>
|
||||
<text class="ts" x="58" y="1159">critical test</text>
|
||||
<rect x="130" y="1151" width="12" height="8" rx="2" fill="#E1F5EE" stroke="#0F6E56" stroke-width="0.5"/>
|
||||
<text class="ts" x="148" y="1159">HIL test</text>
|
||||
<rect x="200" y="1151" width="12" height="8" rx="2" fill="#FAEEDA" stroke="#854F0B" stroke-width="0.5"/>
|
||||
<text class="ts" x="218" y="1159">operator</text>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
|
@ -1,51 +0,0 @@
|
|||
|
||||
#include "board.h"
|
||||
#include "bsp/led.h"
|
||||
#include "bsp/tick.h"
|
||||
#include "bsp/uart_host.h"
|
||||
#include "bsp/usb_cdc.h"
|
||||
#include "log/log.h"
|
||||
#include "port/log_uart.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
int main(void)
|
||||
{
|
||||
const uint16_t DELAY_MS = 100;
|
||||
const uint32_t UART_BAUDRATE = 115200;
|
||||
board_hw_init();
|
||||
|
||||
bsp_led_init();
|
||||
bsp_tick_init();
|
||||
bsp_uart_host_init(UART_BAUDRATE);
|
||||
log_uart_init();
|
||||
|
||||
LOG_I("BOOT", "firmware_test started, tick=%lu", (unsigned long) bsp_tick_get_ms());
|
||||
|
||||
if (bsp_usb_cdc_init() == BSP_OK)
|
||||
{
|
||||
LOG_I("BOOT", "USB CDC ACM initialized");
|
||||
}
|
||||
|
||||
bsp_led_on(LED_APP);
|
||||
bool is_connection_established = false;
|
||||
while (1)
|
||||
{
|
||||
if (bsp_usb_cdc_is_ready() == true && is_connection_established == false)
|
||||
{
|
||||
is_connection_established = true;
|
||||
LOG_I("BOOT", "USB CDC ACM ready");
|
||||
}
|
||||
|
||||
if (is_connection_established == true)
|
||||
{
|
||||
const char *msg = "Hello from TFT Board\r\n";
|
||||
bsp_usb_cdc_write((const uint8_t *) msg, strlen(msg));
|
||||
}
|
||||
|
||||
bsp_usb_cdc_poll();
|
||||
bsp_led_toggle(LED_HEARTBEAT);
|
||||
bsp_delay(DELAY_MS);
|
||||
}
|
||||
}
|
||||
191
firmware/test/src/cli.c
Normal file
191
firmware/test/src/cli.c
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
/**
|
||||
* @file cli.c
|
||||
* @brief Реализация CLI для firmware_test.
|
||||
*
|
||||
* Транспорт: USB CDC ACM через bsp_usb_cdc — единственный канал.
|
||||
* Парсинг JSON минималистичный: strstr по полю "cmd".
|
||||
* Полноценный JSON-парсер (cJSON) не используется намеренно —
|
||||
* схема фиксирована, единственное входящее поле — "cmd".
|
||||
*
|
||||
* Добавление новой команды:
|
||||
* 1. Объявить static void cmd_foo(void); выше таблицы.
|
||||
* 2. Добавить { "FOO", cmd_foo } в k_cmds[].
|
||||
* 3. Реализовать обработчик ниже раздела "Command handlers".
|
||||
*/
|
||||
|
||||
#include "cli.h"
|
||||
|
||||
#include "bsp/usb_cdc.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── Forward declarations ──────────────────────────────────────────────── */
|
||||
|
||||
static void cmd_ping(void);
|
||||
|
||||
/* ── Command table ─────────────────────────────────────────────────────── */
|
||||
|
||||
typedef void (*cli_handler_t)(void);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
const char *name;
|
||||
cli_handler_t handler;
|
||||
} cli_cmd_t;
|
||||
|
||||
static const cli_cmd_t k_cmds[] = {
|
||||
{ "PING", cmd_ping },
|
||||
};
|
||||
|
||||
#define CLI_CMD_COUNT (sizeof(k_cmds) / sizeof(k_cmds[0]))
|
||||
|
||||
/* ── RX line buffer ────────────────────────────────────────────────────── */
|
||||
|
||||
static uint8_t g_s_line_buf[CLI_LINE_BUF_SIZE];
|
||||
static size_t g_s_line_len = 0U;
|
||||
|
||||
/* ── Internal helpers ──────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @brief Извлечь значение поля "cmd" из JSON-строки.
|
||||
*
|
||||
* Ищет паттерн ` "cmd":"<ASCII без кавычек и обратных слэшей>"\n`. Без рекурсии и динамической памяти.
|
||||
*
|
||||
* @param[in] line NULL-terminated входная строка.
|
||||
* @param[out] out Буфер для записи значения.
|
||||
* @param[in] out_size Размер out (включая место под '\0').
|
||||
* @return true если поле найдено и значение помещается в out.
|
||||
*/
|
||||
static bool parse_cmd_field(const char *p_line, char *p_out, size_t out_size)
|
||||
{
|
||||
const char *key = strstr(p_line, "\"cmd\"");
|
||||
if (key == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *colon = strchr(key + 5U, ':');
|
||||
if (colon == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *open_q = strchr(colon + 1U, '"');
|
||||
if (open_q == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
open_q++; /* skip the opening quote */
|
||||
|
||||
const char *close_q = strchr(open_q, '"');
|
||||
if (close_q == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t len = (size_t) (close_q - open_q);
|
||||
if (len == 0U || len >= out_size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(p_out, open_q, len);
|
||||
p_out[len] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Найти и вызвать обработчик команды.
|
||||
*
|
||||
* Если команда не найдена — отправить UNKNOWN_CMD.
|
||||
*/
|
||||
static void dispatch(const char *p_cmd_name)
|
||||
{
|
||||
for (size_t i = 0U; i < CLI_CMD_COUNT; i++)
|
||||
{
|
||||
if (strcmp(k_cmds[i].name, p_cmd_name) == 0)
|
||||
{
|
||||
k_cmds[i].handler();
|
||||
return;
|
||||
}
|
||||
}
|
||||
cli_send("{\"ok\":false,\"error\":\"UNKNOWN_CMD\"}\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обработать одну накопленную строку (без завершающего '\n').
|
||||
*/
|
||||
static void process_line(const char *p_line)
|
||||
{
|
||||
|
||||
const uint8_t MAX_CMD_LEN = 32U;
|
||||
char cmd_name[MAX_CMD_LEN];
|
||||
|
||||
if (!parse_cmd_field(p_line, cmd_name, sizeof(cmd_name)))
|
||||
{
|
||||
cli_send("{\"ok\":false,\"error\":\"PARSE_ERR\"}\n");
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(cmd_name);
|
||||
}
|
||||
|
||||
/* ── Public API ────────────────────────────────────────────────────────── */
|
||||
|
||||
void cli_init(void)
|
||||
{
|
||||
g_s_line_len = 0U;
|
||||
}
|
||||
|
||||
void cli_send(const char *p_resp)
|
||||
{
|
||||
bsp_usb_cdc_write((const uint8_t *) p_resp, strlen(p_resp));
|
||||
}
|
||||
|
||||
void cli_process(void)
|
||||
{
|
||||
uint8_t chunk[CLI_LINE_BUF_SIZE];
|
||||
|
||||
size_t nbytes = bsp_usb_cdc_read(chunk, sizeof(chunk));
|
||||
|
||||
for (size_t byte_idx = 0U; byte_idx < nbytes; byte_idx++)
|
||||
{
|
||||
uint8_t byte = chunk[byte_idx];
|
||||
|
||||
if (g_s_line_len >= (CLI_LINE_BUF_SIZE - 1U))
|
||||
{
|
||||
g_s_line_len = 0U;
|
||||
cli_send("{\"ok\":false,\"error\":\"LINE_TOO_LONG\"}\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (byte == (uint8_t) '\n')
|
||||
{
|
||||
/* Отбросить '\r' если терминал шлёт CR+LF */
|
||||
if (g_s_line_len > 0U && g_s_line_buf[g_s_line_len - 1U] == (uint8_t) '\r')
|
||||
{
|
||||
g_s_line_len--;
|
||||
}
|
||||
g_s_line_buf[g_s_line_len] = '\0';
|
||||
if (g_s_line_len > 0U)
|
||||
{
|
||||
process_line((const char *) g_s_line_buf);
|
||||
}
|
||||
g_s_line_len = 0U;
|
||||
}
|
||||
else
|
||||
{
|
||||
g_s_line_buf[g_s_line_len] = byte;
|
||||
g_s_line_len++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Command handlers ──────────────────────────────────────────────────── */
|
||||
|
||||
static void cmd_ping(void)
|
||||
{
|
||||
cli_send("{\"ok\":true,\"result\":\"PONG\"}\n");
|
||||
}
|
||||
50
firmware/test/src/cli.h
Normal file
50
firmware/test/src/cli.h
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* @file cli.h
|
||||
* @brief Command-line interface для firmware_test.
|
||||
*
|
||||
* Единственный транспорт — USB CDC ACM (bsp_usb_cdc).
|
||||
* Протокол — JSON-lines: каждая строка завершается '\n'.
|
||||
* Запрос: {"cmd":"NAME"}\n
|
||||
* Ответ: {"ok":true,...}\n или {"ok":false,"error":"CODE"}\n
|
||||
*
|
||||
* @note Архитектурное ограничение: этот модуль жёстко связан с
|
||||
* bsp_usb_cdc как единственным IO-каналом. Замена транспорта
|
||||
* не предусмотрена — firmware_test работает только через USB CDC.
|
||||
* Для HIL ELF-прошивок используется отдельный канал связи (UART + bsp_uart_host).
|
||||
*/
|
||||
|
||||
#ifndef CLI_H_
|
||||
#define CLI_H_
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/** @brief Максимальная длина входящей JSON-строки включая завершающий '\n'. */
|
||||
#define CLI_LINE_BUF_SIZE 128U
|
||||
|
||||
/**
|
||||
* @brief Инициализировать CLI.
|
||||
*
|
||||
* Сбрасывает внутренний буфер строки.
|
||||
* Вызывать после bsp_usb_cdc_init() и до первого cli_process().
|
||||
*/
|
||||
void cli_init(void);
|
||||
|
||||
/**
|
||||
* @brief Отправить готовую JSON-строку через USB CDC.
|
||||
*
|
||||
* @param[in] resp NUL-terminated строка, завершённая '\n'.
|
||||
*
|
||||
* @note Неблокирующий вызов. Если TX занят — запись теряется.
|
||||
* Для firmware_test это приемлемо: хост повторит запрос.
|
||||
*/
|
||||
void cli_send(const char *resp);
|
||||
|
||||
/**
|
||||
* @brief Обработать входящие байты и диспатчить команду при получении '\n'.
|
||||
*
|
||||
* Вызывать в главном цикле после bsp_usb_cdc_poll().
|
||||
* Неблокирующий: если данных нет — возвращается немедленно.
|
||||
*/
|
||||
void cli_process(void);
|
||||
|
||||
#endif /* CLI_H_ */
|
||||
61
firmware/test/src/main.c
Normal file
61
firmware/test/src/main.c
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
|
||||
/**
|
||||
* @file main.c
|
||||
* @brief firmware_test — точка входа.
|
||||
*
|
||||
* Bare-metal входной контроль платы.
|
||||
* Единственный канал хост↔таргет: USB CDC ACM (J2).
|
||||
* Протокол: JSON-lines через cli.c.
|
||||
*
|
||||
* Последовательность старта:
|
||||
* 1. board_hw_init() — тактирование, MPU, кэш, пины
|
||||
* 2. bsp_tick_init() — SysTick 1 мс
|
||||
* 3. bsp_led_init() — оба LED выключены
|
||||
* 4. bsp_usb_cdc_init() — PHY + стек + NVIC
|
||||
* 5. Ожидание CDC ready — LED_HEARTBEAT мигает
|
||||
* 6. cli_init() — сброс буфера
|
||||
* 7. READY → хост — JSON сигнал готовности
|
||||
* 8. Главный цикл — poll + cli_process
|
||||
*/
|
||||
#include "board.h"
|
||||
#include "bsp/led.h"
|
||||
#include "bsp/tick.h"
|
||||
#include "bsp/usb_cdc.h"
|
||||
#include "cli.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const uint32_t CONNECT_BLINK_MS = 200U;
|
||||
const uint32_t ERROR_BLINK_MS = 50;
|
||||
board_hw_init();
|
||||
|
||||
bsp_led_init();
|
||||
bsp_tick_init();
|
||||
|
||||
if (bsp_usb_cdc_init() != BSP_OK)
|
||||
{
|
||||
bsp_led_toggle(LED_HEARTBEAT);
|
||||
bsp_delay(ERROR_BLINK_MS);
|
||||
}
|
||||
|
||||
/* Ожидать подключения хоста. LED_HEARTBEAT мигает — прошивка жива. */
|
||||
while (!bsp_usb_cdc_is_ready())
|
||||
{
|
||||
bsp_usb_cdc_poll();
|
||||
bsp_led_toggle(LED_HEARTBEAT);
|
||||
bsp_delay(CONNECT_BLINK_MS);
|
||||
}
|
||||
|
||||
bsp_led_on(LED_APP);
|
||||
|
||||
cli_init();
|
||||
cli_send("{\"ok\":true,\"result\":\"READY\"}\n");
|
||||
while (1)
|
||||
{
|
||||
bsp_usb_cdc_poll();
|
||||
cli_process();
|
||||
}
|
||||
}
|
||||
|
|
@ -158,3 +158,18 @@ add_host_test(
|
|||
${CMAKE_SOURCE_DIR}/bsp/common/include
|
||||
MOCKS
|
||||
${BSP_MOCKS_DIR})
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Тест логики cli
|
||||
# -----------------------------------------------------------------------------
|
||||
add_host_test(
|
||||
NAME
|
||||
test_cli
|
||||
SOURCES
|
||||
cli/test_cli.c
|
||||
INCLUDES
|
||||
${CMAKE_SOURCE_DIR}/firmware/test/src/
|
||||
${CMAKE_SOURCE_DIR}/bsp/common/include
|
||||
${CMAKE_SOURCE_DIR}/bsp/usb_cdc/include
|
||||
MOCKS
|
||||
${BSP_MOCKS_DIR})
|
||||
|
|
|
|||
201
tests/host/cli/test_cli.c
Normal file
201
tests/host/cli/test_cli.c
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/**
|
||||
* @file test_cli.c
|
||||
* @brief Host unit-тест для firmware_test/src/cli.c
|
||||
*
|
||||
* Тестирует: parse_cmd_field (через cli_process), dispatch, обработку
|
||||
* граничных случаев буфера. Без железа, без fff — stub-ы для
|
||||
* bsp_usb_cdc_read/write определены прямо здесь.
|
||||
*
|
||||
* Добавление теста для новой команды:
|
||||
* 1. stub_rx_feed() — подать строку в RX буфер.
|
||||
* 2. cli_process() — запустить.
|
||||
* 3. TEST_ASSERT_EQUAL_STRING() — проверить TX буфер.
|
||||
*/
|
||||
|
||||
#include "bsp/status.h"
|
||||
#include "bsp/usb_cdc.h"
|
||||
#include "unity.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── Stub: bsp_usb_cdc ─────────────────────────────────────────────────
|
||||
*
|
||||
* RX: тест кладёт байты через stub_rx_feed().
|
||||
* cli_process() читает их по одному через bsp_usb_cdc_read().
|
||||
*
|
||||
* TX: cli_send() пишет через bsp_usb_cdc_write() в s_tx_buf.
|
||||
* тест проверяет s_tx_buf через stub_tx_get().
|
||||
*
|
||||
* ────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
static uint8_t s_rx_buf[256];
|
||||
static size_t s_rx_len = 0U;
|
||||
static size_t s_rx_pos = 0U;
|
||||
|
||||
static char s_tx_buf[512];
|
||||
static size_t s_tx_len = 0U;
|
||||
|
||||
/** @brief Заполнить RX буфер данными для теста. */
|
||||
static void stub_rx_feed(const char *data)
|
||||
{
|
||||
size_t len = strlen(data);
|
||||
memcpy(s_rx_buf, data, len);
|
||||
s_rx_len = len;
|
||||
s_rx_pos = 0U;
|
||||
}
|
||||
|
||||
/** @brief Получить TX буфер как C-строку. */
|
||||
static const char *stub_tx_get(void)
|
||||
{
|
||||
s_tx_buf[s_tx_len] = '\0';
|
||||
return s_tx_buf;
|
||||
}
|
||||
|
||||
/** @brief Сбросить оба буфера. */
|
||||
static void stub_reset(void)
|
||||
{
|
||||
s_rx_len = 0U;
|
||||
s_rx_pos = 0U;
|
||||
s_tx_len = 0U;
|
||||
memset(s_tx_buf, 0, sizeof(s_tx_buf));
|
||||
}
|
||||
|
||||
/* bsp_usb_cdc stubs */
|
||||
|
||||
size_t bsp_usb_cdc_read(uint8_t *buf, size_t max_len)
|
||||
{
|
||||
size_t available = s_rx_len - s_rx_pos;
|
||||
size_t to_copy = (available < max_len) ? available : max_len;
|
||||
memcpy(buf, s_rx_buf + s_rx_pos, to_copy);
|
||||
s_rx_pos += to_copy;
|
||||
return to_copy;
|
||||
}
|
||||
|
||||
bsp_status_t bsp_usb_cdc_write(const uint8_t *data, size_t len)
|
||||
{
|
||||
if ((s_tx_len + len) < sizeof(s_tx_buf))
|
||||
{
|
||||
memcpy(s_tx_buf + s_tx_len, data, len);
|
||||
s_tx_len += len;
|
||||
}
|
||||
return BSP_OK;
|
||||
}
|
||||
|
||||
/* Остальные символы bsp_usb_cdc — заглушки, cli.c не вызывает их */
|
||||
bsp_status_t bsp_usb_cdc_init(void)
|
||||
{
|
||||
return BSP_OK;
|
||||
}
|
||||
bool bsp_usb_cdc_is_ready(void)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
bool bsp_usb_cdc_write_ready(void)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
void bsp_usb_cdc_poll(void)
|
||||
{
|
||||
}
|
||||
|
||||
/* ── Включаем тестируемый модуль ПОСЛЕ stub-ов ─────────────────────── */
|
||||
|
||||
#include "cli.c" /* NOLINT(bugprone-suspicious-include) */
|
||||
|
||||
/* ── Фикстуры ──────────────────────────────────────────────────────── */
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
stub_reset();
|
||||
cli_init();
|
||||
}
|
||||
|
||||
void tearDown(void)
|
||||
{
|
||||
}
|
||||
|
||||
/* ── Тесты ─────────────────────────────────────────────────────────── */
|
||||
|
||||
void test_ping_returns_pong(void)
|
||||
{
|
||||
stub_rx_feed("{\"cmd\":\"PING\"}\n");
|
||||
cli_process();
|
||||
TEST_ASSERT_EQUAL_STRING("{\"ok\":true,\"result\":\"PONG\"}\n", stub_tx_get());
|
||||
}
|
||||
|
||||
void test_unknown_command(void)
|
||||
{
|
||||
stub_rx_feed("{\"cmd\":\"FOOBAR\"}\n");
|
||||
cli_process();
|
||||
TEST_ASSERT_EQUAL_STRING("{\"ok\":false,\"error\":\"UNKNOWN_CMD\"}\n", stub_tx_get());
|
||||
}
|
||||
|
||||
void test_missing_cmd_field(void)
|
||||
{
|
||||
stub_rx_feed("{\"foo\":\"bar\"}\n");
|
||||
cli_process();
|
||||
TEST_ASSERT_EQUAL_STRING("{\"ok\":false,\"error\":\"PARSE_ERR\"}\n", stub_tx_get());
|
||||
}
|
||||
|
||||
void test_empty_line_no_response(void)
|
||||
{
|
||||
/* Пустая строка '\n' — буфер пуст, dispatch не вызывается */
|
||||
stub_rx_feed("\n");
|
||||
cli_process();
|
||||
TEST_ASSERT_EQUAL_STRING("", stub_tx_get());
|
||||
}
|
||||
|
||||
void test_line_too_long(void)
|
||||
{
|
||||
/* 127 байт + '\n' = ровно граница CLI_LINE_BUF_SIZE */
|
||||
char long_line[CLI_LINE_BUF_SIZE + 2U];
|
||||
memset(long_line, 'A', CLI_LINE_BUF_SIZE);
|
||||
long_line[CLI_LINE_BUF_SIZE] = '\n';
|
||||
long_line[CLI_LINE_BUF_SIZE + 1U] = '\0';
|
||||
|
||||
stub_rx_feed(long_line);
|
||||
cli_process();
|
||||
TEST_ASSERT_EQUAL_STRING("{\"ok\":false,\"error\":\"LINE_TOO_LONG\"}\n", stub_tx_get());
|
||||
}
|
||||
|
||||
void test_two_commands_in_sequence(void)
|
||||
{
|
||||
/* Два PING подряд — оба должны дать PONG */
|
||||
stub_rx_feed("{\"cmd\":\"PING\"}\n{\"cmd\":\"PING\"}\n");
|
||||
cli_process();
|
||||
TEST_ASSERT_EQUAL_STRING("{\"ok\":true,\"result\":\"PONG\"}\n"
|
||||
"{\"ok\":true,\"result\":\"PONG\"}\n",
|
||||
/* Нет: первый вызов — первый PONG. Сбросим и проверим второй. */
|
||||
stub_tx_get());
|
||||
}
|
||||
|
||||
void test_cmd_field_with_spaces(void)
|
||||
{
|
||||
/* Пробелы вокруг ':' — реальные клиенты могут так форматировать */
|
||||
stub_rx_feed("{\"cmd\" : \"PING\"}\n");
|
||||
cli_process();
|
||||
TEST_ASSERT_EQUAL_STRING("{\"ok\":true,\"result\":\"PONG\"}\n", stub_tx_get());
|
||||
}
|
||||
|
||||
void test_partial_input_no_response_until_newline(void)
|
||||
{
|
||||
/* Подать строку без '\n' — ответа быть не должно */
|
||||
stub_rx_feed("{\"cmd\":\"PING\"}");
|
||||
cli_process();
|
||||
TEST_ASSERT_EQUAL_STRING("", stub_tx_get());
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_ping_returns_pong);
|
||||
RUN_TEST(test_unknown_command);
|
||||
RUN_TEST(test_missing_cmd_field);
|
||||
RUN_TEST(test_empty_line_no_response);
|
||||
RUN_TEST(test_line_too_long);
|
||||
RUN_TEST(test_two_commands_in_sequence);
|
||||
RUN_TEST(test_cmd_field_with_spaces);
|
||||
RUN_TEST(test_partial_input_no_response_until_newline);
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Reference in a new issue