20 KiB
firmware_test — Протокол диагностики v2
Расположение в репозитории:
docs/testing/PROTOCOL.mdДокумент описывает протокол обмена между диагностической прошивкой (
firmware_test) и хостовым ПО сервисного инженера. Актуален для:firmware_test v0.1.0+,protocol.h v2.
Назначение и контекст
firmware_test — специализированная прошивка для диагностики плат
MIMXRT1052CVJ5B, вернувшихся по рекламации. Запускается сервисным
инженером через USB CDC ACM (разъём J2). Загружается через BootROM (USB SDP)
без предварительной прошивки загрузчика.
flowchart TB
Host["Хост-ПК\nсервисного инженера"]
FW["Плата MIMXRT1052\nfirmware_test"]
Periph["Периферия\nSDRAM · QSPI Flash · uSD\nDisplay · CAN · UART · Opto"]
M5["M5StampPLC\nвнешние сигналы для HIL"]
Host -->|"USB CDC ACM J2\nJSON-lines, 1 строка = 1 сообщение"| FW
FW -->|"GPIO / LPUART / SEMC\nFlexSPI / USDHC"| Periph
M5 -->|"реле → оптовходы / CAN / UART"| FW
Принцип работы: вся тест-логика живёт на таргете (test_runner.c,
tests/*.c). Хост — тонкий клиент: отправляет команды, отображает события,
управляет интерактивными шагами. Инженер запускает тесты атомарно или все
подряд (run_all).
Транспорт
| Параметр | Значение |
|---|---|
| Интерфейс | USB CDC ACM |
| Разъём | J2 |
| Кодировка | UTF-8 |
| Фреймирование | JSON-lines: каждое сообщение — одна строка, завершается \n |
| Максимальная длина строки | 128 байт (включая \n) |
| CR+LF | Принимается (таргет отбрасывает \r перед \n) |
| Направление | Двунаправленный, half-duplex по логике |
Нет хэндшейка, нет sequence number, нет подтверждений доставки.
Формат сообщений
Все сообщения — JSON-объекты в одну строку (\n в конце). Поле "type"
определяет смысл сообщения:
Хост → Таргет: "type": "cmd" — команда
"type": "confirm" — ответ оператора на интерактивный шаг
Таргет → Хост: "type": "session_start" — таргет готов
"type": "pong" — ответ на ping
"type": "test_begin" — тест стартовал
"type": "progress" — промежуточный шаг теста
"type": "test_result" — тест завершён
"type": "confirm_request" — ожидание действия оператора
"type": "summary" — итог run_all
"ok": false, "error": "…" — ошибка протокола
Жизненный цикл сессии
sequenceDiagram
participant H as Хост
participant T as Таргет
Note over T: прошивка загружена через USB SDP
T-->>H: {"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
H->>T: {"type":"cmd","cmd":"ping"}
T-->>H: {"type":"pong"}
Note over H: инженер выбирает тест
H->>T: {"type":"cmd","cmd":"run","id":"sdram"}
T-->>H: {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
T-->>H: {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
H->>T: {"type":"cmd","cmd":"run_all"}
T-->>H: {"type":"test_begin","id":"sdram",...}
T-->>H: {"type":"test_result","id":"sdram","status":"pass",...}
Note over T: ...каждый тест в реестре...
T-->>H: {"type":"summary","overall":"pass","passed":7,"failed":0,"skipped":1}
session_start отправляется автоматически при каждом старте таргета, до
получения первой команды.
Команды хоста → таргет
ping
→ {"type":"cmd","cmd":"ping"}
← {"type":"pong"}
run — запуск одного теста
→ {"type":"cmd","cmd":"run","id":"sdram"}
← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
Если id не найден: ← {"ok":false,"error":"UNKNOWN_TEST"}
run_all — запуск всех тестов
Если тест помечен "critical":true и вернул "fail" — выполнение
прерывается, остальные получают "skip".
→ {"type":"cmd","cmd":"run_all"}
← {"type":"test_begin","id":"sdram",...}
← {"type":"test_result","id":"sdram","status":"pass",...}
← ... (каждый тест в реестре) ...
← {"type":"summary","passed":7,"failed":0,"skipped":1,"overall":"pass"}
list_tests — получить список тестов
Таргет возвращает реестр тестов с метаданными. TUI строит список динамически на основе этого ответа, не хардкодит тесты.
→ {"type":"cmd","cmd":"list_tests"}
← {"type":"test_list","tests":[
{"id":"sdram","name":"SDRAM 32 MB","critical":true,"requires_hil":false},
{"id":"qspi","name":"QSPI Flash W25Qxx","critical":true,"requires_hil":false},
{"id":"usd","name":"microSD (SDIO)","critical":false,"requires_hil":false},
{"id":"display","name":"TFT Display RGB888","critical":false,"requires_hil":false},
{"id":"buttons","name":"Test Buttons","critical":false,"requires_hil":false},
{"id":"mqs","name":"MQS Audio Out","critical":false,"requires_hil":false},
{"id":"can","name":"CAN loopback","critical":false,"requires_hil":true},
{"id":"opto","name":"Opto Inputs","critical":false,"requires_hil":true}
]}
get_uid — чтение уникального идентификатора чипа
Запрос UID из OCOTP. Может быть отправлен в любой момент когда runner не BUSY.
→ {"type":"cmd","cmd":"get_uid"}
← {"type":"uid_response","uid":"AABBCCDDEEFF0011"}
uid — 16 hex-символов (8 байт big-endian): CFG1[63:32] + CFG0[31:0].
При ошибке чтения OCOTP:
← {"ok":false,"error":"UID_READ_ERR"}
run_selected — запуск подмножества тестов
Запускает тесты по списку ID. Порядок выполнения — по реестру таргета,
не по порядку в запросе. Таргет не фильтрует по requires_hil —
ответственность за фильтрацию HIL-тестов лежит на TUI.
→ {"type":"cmd","cmd":"run_selected","tests":["sdram","qspi","display"]}
← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
← {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
← {"type":"test_begin","id":"qspi",...}
← {"type":"test_result","id":"qspi",...}
← {"type":"test_begin","id":"display",...}
← {"type":"test_result","id":"display",...}
← {"type":"summary","passed":3,"failed":0,"skipped":0,"overall":"pass"}
Если хотя бы один ID не найден в реестре — ни один тест не запускается:
← {"ok":false,"error":"UNKNOWN_TEST"}
События таргета → хост
session_start
{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}
test_begin
{"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true}
test_result
{"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
status |
Смысл |
|---|---|
"pass" |
Тест пройден |
"fail" |
Тест провален; detail содержит описание |
"skip" |
Пропущен (нет оборудования, таймаут оператора, прерван critical fail) |
detail — ASCII-строка до 95 символов. При pass — пустая.
progress
{"type":"progress","test":"usd","step":"mount","status":"ok"}
Промежуточные шаги внутри теста. Используется в usd.
confirm_request
{
"type": "confirm_request",
"id": "display_red",
"prompt": "Экран залит красным цветом?",
"timeout_ms": 15000
}
Хост отображает prompt оператору. Авторитетный таймаут — на таргете;
если оператор не ответил за timeout_ms — таргет переходит в SKIP.
summary
{"type":"summary","passed":6,"failed":1,"skipped":0,"overall":"fail"}
"overall": "fail" если хотя бы один critical тест провален.
confirm (хост → таргет)
→ {"type":"confirm","id":"display_red","confirmed":true}
"id" должен совпадать с id из confirm_request. Ответ после timeout_ms
игнорируется.
Ошибки протокола
| Код | Причина |
|---|---|
PARSE_ERR |
Строка не является валидным JSON-lines запросом |
UNKNOWN_CMD |
Поле "cmd" содержит неизвестное значение |
UNKNOWN_TEST |
Поле "id" в run или "tests" в run_selected содержит неизвестный ID |
LINE_TOO_LONG |
Входящая строка превысила 128 байт |
BUSY |
Таргет выполняет тест, новая команда отклонена |
Матрица тестов
| 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 | ❌ | ✅ | ❌ |
Типы: self — таргет тестирует периферию самостоятельно; interactive —
требует confirm_request; HIL — требует M5StampPLC.
Интерактивные тесты — детальный поток
uSD
sequenceDiagram
participant H as Хост
participant T as Таргет
T-->>H: {"type":"confirm_request","id":"usd","prompt":"Insert microSD card","timeout_ms":30000}
H->>T: {"type":"confirm","id":"usd","confirmed":true}
T-->>H: {"type":"test_begin","id":"usd","name":"microSD (SDIO)","critical":false}
T-->>H: {"type":"progress","test":"usd","step":"card_detect","status":"ok"}
T-->>H: {"type":"progress","test":"usd","step":"mount","status":"ok"}
T-->>H: {"type":"progress","test":"usd","step":"write","status":"ok"}
T-->>H: {"type":"progress","test":"usd","step":"read_compare","status":"ok"}
T-->>H: {"type":"test_result","id":"usd","status":"pass","ms":741,"detail":""}
При отказе ("confirmed":false) или таймауте:
← {"type":"test_begin","id":"usd","name":"microSD (SDIO)","critical":false}
← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"operator declined"}
Display (RGB888)
sequenceDiagram
participant H as Хост
participant T as Таргет
T-->>H: {"type":"test_begin","id":"display",...}
T-->>H: {"type":"confirm_request","id":"display_red","prompt":"Экран залит красным?","timeout_ms":15000}
H->>T: {"type":"confirm","id":"display_red","confirmed":true}
T-->>H: {"type":"confirm_request","id":"display_green","prompt":"Экран залит зелёным?","timeout_ms":15000}
H->>T: {"type":"confirm","id":"display_green","confirmed":true}
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"}
Кнопки
sequenceDiagram
participant H as Хост
participant T as Таргет
T-->>H: {"type":"test_begin","id":"buttons",...}
T-->>H: {"type":"confirm_request","id":"btn1_press","prompt":"Нажмите кнопку Test_But_1","timeout_ms":10000}
Note over T: ждёт bsp_button_get(BTN_TEST_1) == PRESSED<br/>таймаут 10 с
T-->>H: {"type":"confirm_request","id":"btn2_press","prompt":"Нажмите кнопку Test_But_2","timeout_ms":10000}
Note over T: ждёт bsp_button_get(BTN_TEST_2) == PRESSED
T-->>H: {"type":"test_result","id":"buttons","status":"pass","ms":3821,"detail":""}
Важно: для теста кнопок таргет не ждёт
{"type":"confirm",…}от хоста. Нажатие детектируется прошивкой черезbsp_button. Хост отображаетpromptи ждёт следующего события от таргета.
State machine test_runner
stateDiagram-v2
[*] --> IDLE
IDLE --> PRE_CONFIRM : cmd run / run_all
note right of PRE_CONFIRM : pre_confirm_prompt != NULL?\nprotocol_send_confirm_request()
PRE_CONFIRM --> RUNNING : confirm получен или prompt == NULL
RUNNING --> RUNNING : run_all — следующий тест
note right of RUNNING : protocol_send_test_begin()\nmod->init()\nresult = mod->run()\nmod->deinit()\nprotocol_send_test_result()
RUNNING --> IDLE : run завершён
RUNNING --> IDLE : run_all завершён\nprotocol_send_summary()
Реализация на стороне таргета
firmware/test/src/
├── main.c — инициализация, главный цикл, вызов cli_process()
├── cli.h / cli.c — IO-слой: буферизация строк, диспатч по "type"
├── protocol.h / .c — сериализация исходящих событий через cli_send()
├── test_module.h — интерфейс тест-модуля (struct test_module_t)
├── test_runner.h/.c — реестр тестов, state machine, confirm механизм
└── tests/
├── test_sdram.c
├── test_qspi.c
├── test_usd.c
├── test_display.c
├── test_buttons.c
├── test_can.c
├── test_uart_ttl.c
├── test_uart_iso.c
└── test_opto.c
Добавление нового теста
- Создать
firmware/test/src/tests/test_foo.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,
};
- Добавить
&k_test_fooв реестрtest_runner.c. - Добавить
tests/test_foo.cвCMakeLists.txtтаргета.
Рекомендации для разработчика хостового ПО
Открытие порта: найти CDC ACM устройство → открыть порт → ждать
"type":"session_start" (таймаут 10 с) → при отсутствии переоткрыть порт.
Чтение событий: буферизировать до '\n'; одна строка = одно сообщение;
неизвестный "type" — игнорировать (forward-compatibility).
Отправка команд: завершать каждую строку '\n' (не '\r\n'); не
отправлять следующую команду до test_result или error от предыдущей.
Исключение: "ping" можно отправлять в любой момент, если таргет не BUSY.
Обработка confirm_request: отобразить prompt → ждать реакции
оператора → отправить {"type":"confirm","id":"<тот же id>","confirmed":true/false}.
Исключение — тест buttons: не отправлять confirm, просто ждать следующего
события от таргета.
Версионирование протокола
Поле "fw" в session_start — версия прошивки. При несовместимых изменениях
протокола — bump FIRMWARE_TEST_VERSION в protocol.h с обновлением этого
документа. Хост должен проверять "fw" и предупреждать оператора при
несовпадении ожидаемой версии.