lift_indicator_suite/firmware/test/README.md

30 KiB
Raw Permalink Blame History

firmware_test

Диагностическая прошивка для плат MIMXRT1052CVJ5B, вернувшихся по рекламации. Запускается сервисным инженером через USB CDC ACM без предварительной прошивки загрузчика.

Версия прошивки: 0.1.0 | Протокол: v2


Содержание


Быстрый старт

1. Сборка (devcontainer)

just build::build-firmware-test-debug
just build::hab-firmware-test-debug

2. Прошивка (хост)

# Перевести плату в SDP-режим: BOOT_MOD_1 → 3V3 → Reset
just host::flash-test-debug

# Или через SWD (power cycle после)
just host::flash-swd-test-debug

3. Подключение

Подключить USB к разъёму J2 (USB CDC ACM). Открыть любой терминал:

# macOS
screen /dev/cu.usbmodemXXXX

# Linux
screen /dev/ttyACM0

4. Работа с прошивкой

После подключения таргет сразу присылает:

{"type":"session_start","fw":"0.1.0","target":"IMXRT1052","uptime_ms":0}

Проверка связи:

 {"type":"cmd","cmd":"ping"}
 {"type":"pong"}

Запуск одного теста:

 {"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":""}

Запуск всех тестов:

 {"type":"cmd","cmd":"run_all"}
 ... (события каждого теста) ...
 {"type":"summary","passed":7,"failed":0,"skipped":1,"overall":"pass"}

5. Host unit-тесты (devcontainer)

just build::test-host

Архитектура

Стенд

[Хост-ПК сервисного инженера]
        │  USB CDC ACM (J2) — единственный канал
        │  JSON-lines, 1 строка = 1 сообщение
        ▼
[Плата MIMXRT1052 с firmware_test]
        │  GPIO / LPUART / SEMC / FlexSPI / USDHC
        ▼
[Периферия: SDRAM, QSPI Flash, uSD, Display, Кнопки, CAN, UART, Opto][M5StampPLC — управление внешними сигналами для HIL тестов]
        (реле → EXT_IN1/IN2, RS_RX, CAN, UART echo)

Принцип разделения ответственности:

  • Вся тест-логика живёт на таргете (test_runner.c, tests/*.c).
  • Хост — тонкий клиент: отправляет команды, отображает события, управляет интерактивными шагами через confirm.
  • Тесты атомарны: инженер запускает один тест или все сразу — порядок не фиксирован.

Модульная структура

firmware/test/
├── CMakeLists.txt
└── src/
    ├── main.c              — инициализация BSP, главный цикл
    │
    ├── cli.h / cli.c       — IO-слой
    │                         буферизация строк, парсинг "type",
    │                         диспатч на test_runner / protocol
    │
    ├── protocol.h / .c     — сериализация исходящих событий
    │                         все protocol_send_*() → cli_send()
    │
    ├── test_module.h       — интерфейс тест-модуля
    │                         test_module_t, test_result_t,
    │                         confirm_params_t, test_status_t
    │
    ├── test_runner.h / .c  — реестр + state machine
    │                         IDLE → PRE_CONFIRM → RUNNING → IDLE
    │                         test_runner_wait_confirm() для display
    │
    └── tests/
        ├── test_sdram.c    — SDRAM 32 MB (self)
        ├── test_qspi.c     — QSPI Flash 8 MB (self)
        ├── test_usd.c      — uSD SDIO (interactive)
        ├── test_display.c  — Display RGB888 (interactive)
        ├── test_buttons.c  — Test_But_1/2 (interactive)
        ├── test_can.c      — CAN loopback (HIL)
        ├── test_uart_ttl.c — UART TTL (HIL)
        ├── test_uart_iso.c — UART ISO / RS_RX Opto (HIL)
        └── test_opto.c     — Opto-in EXT_IN1/IN2 (HIL)

Граф зависимостей

main.c
  ├── bsp_board          (тактирование, MPU, кэш, пины)
  ├── bsp_tick           (SysTick 1 мс)
  ├── bsp_led            (LED_HEARTBEAT, LED_APP)
  ├── bsp_usb_cdc        (USB CDC ACM, единственный транспорт)
  ├── cli.c
  │     └── bsp_usb_cdc  (read / write)
  │     └── protocol.c   (send_error, send_pong)
  │     └── test_runner.c (run_single, run_all, on_confirm)
  ├── protocol.c
  │     └── cli.c        (cli_send)
  │     └── bsp_tick     (bsp_tick_get_ms — для uptime)
  └── test_runner.c
        └── protocol.c   (все protocol_send_*)
        └── bsp_tick     (bsp_tick_get_ms — таймауты confirm)
        └── bsp_usb_cdc  (bsp_usb_cdc_poll — в wait_confirm)
        └── cli.c        (cli_process — в wait_confirm)
        └── tests/*.c    (тест-модули через реестр)

BSP-зависимости тест-модулей:

Тест BSP модуль
test_sdram bsp_sdram
test_qspi bsp_qspi
test_usd bsp_usd
test_display существующий display BSP
test_buttons bsp_button
test_can bsp_can
test_uart_ttl bsp_uart_host
test_uart_iso bsp_opto (rs_as_gpio=true)
test_opto bsp_opto

State machine test_runner

             cmd: run / run_all
                    │
                    ▼
  ┌─────────────────────────────────────┐
  │             IDLE                    │◄──────────────────────────┐
  │  Ждём команду от хоста              │                           │
  └──────────────────┬──────────────────┘                           │
                     │                                              │
        pre_confirm_prompt != NULL?                                 │
                     │                                              │
          YES        │         NO                                   │
          ▼          │          ▼                                   │
  ┌───────────────┐  │  ┌──────────────────────────────────────┐   │
  │  PRE_CONFIRM  │  │  │  RUNNING                             │   │
  │               │  │  │  protocol_send_test_begin()          │   │
  │  confirm_req  │  │  │  mod->init()   (если задан)          │   │
  │  отправлен,   │  │  │  result = mod->run()  ← блокирует   │   │
  │  ждём JSON    │  │  │  mod->deinit() (если задан)          │   │
  │  от хоста     │  └─►│  protocol_send_test_result()         │   │
  └──────┬────────┘     └──────────────────┬───────────────────┘   │
         │                                 │                        │
    confirmed=true ──────────────────────► │                        │
    confirmed=false → SKIP                 │                        │
    timeout → SKIP                         │                        │
                                           │                        │
                              run:  IDLE ──┘                        │
                              run_all:  следующий тест в реестре ───┘
                              run_all done: protocol_send_summary()

Ключевые свойства state machine:

  • RUNNING — защита от ложного is_busy()==false во время blocking run(). Пока тест выполняется, новые команды получают BUSY.
  • test_runner_wait_confirm() — вызывается из run() интерактивных тестов (display). Внутри polling loop: bsp_usb_cdc_poll() + cli_process(). USB-стек остаётся живым, confirm приходит без возврата в главный цикл.
  • critical=true + FAIL в run_all → все оставшиеся тесты получают SKIP немедленно, summary.overall = "fail".

Протокол v2

Транспорт

Параметр Значение
Интерфейс USB CDC ACM, разъём J2
Кодировка UTF-8
Фреймирование JSON-lines: одна строка = одно сообщение, завершается \n
Максимальная длина строки 128 байт включая \n
CR+LF Принимается (таргет отбрасывает \r)

Нет хэндшейка, нет sequence number, нет подтверждений доставки. Таргет идемпотентен для ping и run — при потере строки хост повторяет.


Жизненный цикл сессии

Хост                                           Таргет
 │                                                │
 │    [USB SDP: прошивка загружена]               │
 │    [CDC ACM: порт открыт]                      │
 │◄─── {"type":"session_start","fw":"0.1.0",...}  │  автоматически
 │                                                │
 │──── {"type":"cmd","cmd":"ping"}  ─────────────►│
 │◄─── {"type":"pong"}                            │
 │                                                │
 │──── {"type":"cmd","cmd":"run","id":"sdram"} ──►│
 │◄─── {"type":"test_begin","id":"sdram",...}     │
 │◄─── {"type":"test_result","id":"sdram",...}    │
 │                                                │
 │──── {"type":"cmd","cmd":"run_all"}  ───────────►│
 │◄─── {"type":"test_begin","id":"sdram",...}     │
 │◄─── {"type":"test_result",...}                 │
 │     … по одному для каждого теста …            │
 │◄─── {"type":"summary","overall":"pass",...}

session_start отправляется автоматически при каждом старте, до получения первой команды. Хост должен быть готов принять его сразу после открытия порта.


Команды хоста → таргет

Все команды имеют "type":"cmd". Поле "cmd" определяет действие.

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 — запуск всех тестов по реестру

 {"type":"cmd","cmd":"run_all"}
 {"type":"test_begin","id":"sdram",...}
 {"type":"test_result","id":"sdram","status":"pass","ms":312,"detail":""}
 {"type":"test_begin","id":"qspi",...}
 {"type":"test_result","id":"qspi","status":"pass","ms":88,"detail":""}
 ... (остальные тесты) ...
 {"type":"summary","passed":7,"failed":0,"skipped":1,"overall":"pass"}

confirm — ответ оператора на интерактивный шаг

 {"type":"confirm","id":"display_red","confirmed":true}

Поле id должно совпадать с id из confirm_request. Ответ после истечения timeout_ms игнорируется — таргет уже перешёл в SKIP.


События таргета → хост

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 содержит описание (до 95 символов)
"skip" Пропущен: нет оборудования, таймаут, отказ оператора, critical fail выше

Примеры detail: "addr=0x80001000 expected=0xA5 got=0x00", "JEDEC ID mismatch".

confirm_request

{
  "type":       "confirm_request",
  "id":         "display_red",
  "prompt":     "Экран залит красным цветом?",
  "timeout_ms": 15000
}

Хост отображает prompt оператору. Авторитетный таймаут — на таргете. Хост может дублировать countdown для UX.

summary

{
  "type":    "summary",
  "passed":  6,
  "failed":  1,
  "skipped": 0,
  "overall": "fail"
}

"overall":"fail" — если хотя бы один critical тест провален. "overall":"pass" — все critical тесты прошли (non-critical могут fail).


Ошибки протокола

 {"ok":false,"error":"PARSE_ERR"}       строка не распознана как JSON-lines
 {"ok":false,"error":"UNKNOWN_CMD"}     неизвестный "cmd" или "type"
 {"ok":false,"error":"UNKNOWN_TEST"}    "id" не найден в реестре
 {"ok":false,"error":"LINE_TOO_LONG"}   строка превысила 128 байт
 {"ok":false,"error":"BUSY"}            таргет выполняет тест

Интерактивные тесты

uSD — вставить карту

{"type":"confirm_request","id":"usd_insert","prompt":"Вставьте microSD","timeout_ms":30000}{"type":"confirm","id":"usd_insert","confirmed":true}{"type":"test_begin","id":"usd",...}{"type":"test_result","id":"usd","status":"pass","ms":541,"detail":""}

Если оператор отказался или таймаут:

{"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"operator skipped"}

Display RGB888 — подтвердить цвета

Четыре шага R/G/B/W. Итог — AND всех подтверждений.

{"type":"test_begin","id":"display",...}{"type":"confirm_request","id":"display_red","prompt":"Экран красный?","timeout_ms":15000}{"type":"confirm","id":"display_red","confirmed":true}{"type":"confirm_request","id":"display_green",...}{"type":"confirm","id":"display_green","confirmed":true}{"type":"confirm_request","id":"display_blue",...}{"type":"confirm","id":"display_blue","confirmed":true}{"type":"confirm_request","id":"display_white",...}{"type":"confirm","id":"display_white","confirmed":false}{"type":"test_result","id":"display","status":"fail","ms":22103,
   "detail":"display_white not confirmed"}

Кнопки — нажать физически

Особый случай: confirm_request используется как инструкция оператору, но хост не отправляет confirm. Таргет сам детектирует нажатие через bsp_button и переходит к следующему событию.

{"type":"test_begin","id":"buttons",...}{"type":"confirm_request","id":"btn1_press","prompt":"Нажмите Test_But_1","timeout_ms":10000}
  [таргет ждёт bsp_button — без JSON confirm от хоста]{"type":"confirm_request","id":"btn2_press","prompt":"Нажмите Test_But_2","timeout_ms":10000}{"type":"test_result","id":"buttons","status":"pass","ms":3821,"detail":""}

Матрица тестов

ID Название Тип Critical M5 HIL Confirm
sdram SDRAM 32 MB self
qspi QSPI Flash 8 MB self
usd uSD SDIO self+interactive pre_confirm
display Display RGB888 interactive 4×в run()
buttons Test_But_1/2 interactive prompt only
can CAN loopback HIL
uart_ttl UART TTL HIL
uart_iso UART ISO / RS_RX HIL
opto Opto EXT_IN1/IN2 HIL

Типы confirm:

  • pre_confirm — test_runner отправляет confirm_request до вызова run(), ждёт JSON-ответ через state machine (асинхронно).
  • в run() — тест сам вызывает test_runner_wait_confirm() изнутри run(), блокируется до ответа (синхронно).
  • prompt onlyprotocol_send_confirm_request() отправляется как UI-подсказка, хост не отвечает JSON, таргет ждёт физического события.

Как добавить новый тест

Шаг 1 — Создать файл теста

/* firmware/test/src/tests/test_foo.c */

#include "test_module.h"

#include "bsp/foo.h"        /* BSP модуль тестируемой периферии */
#include "bsp/usb_cdc.h"    /* bsp_usb_cdc_poll() для длинных тестов */

#include <stdint.h>
#include <stdio.h>

static test_result_t test_foo_run(void)
{
    test_result_t result = { .status = TEST_STATUS_PASS, .duration_ms = 0U };
    result.detail[0] = '\0';

    /* Длинные операции должны периодически звать bsp_usb_cdc_poll(),
     * чтобы USB-стек оставался живым пока run() блокирует главный цикл. */

    bsp_foo_status_t status = bsp_foo_test();

    if (status != BSP_OK)
    {
        result.status = TEST_STATUS_FAIL;
        (void) snprintf(result.detail, TEST_DETAIL_SIZE,
                        "bsp_foo_test returned %d", (int) status);
    }

    return result;
}

const test_module_t k_test_foo = {
    .id                 = "foo",          /* короткий ASCII-ключ         */
    .name               = "Foo Peripheral",
    .critical           = false,          /* true → run_all стопится при fail */
    .requires_hil       = false,          /* true → нужен M5StampPLC     */
    .pre_confirm_prompt = NULL,           /* строка → pre_confirm механизм */
    .init               = NULL,           /* bsp_foo_init если нужен     */
    .run                = test_foo_run,
    .deinit             = NULL,
};

Шаг 2 — Зарегистрировать в реестре

Файл: firmware/test/src/test_runner.c

/* Forward declarations */
extern const test_module_t k_test_sdram;
extern const test_module_t k_test_foo;    /* ← добавить */

static const test_module_t *const k_registry[] = {
    &k_test_sdram,
    &k_test_foo,    /* ← добавить */
};

Шаг 3 — Добавить в CMakeLists.txt

Файл: firmware/test/CMakeLists.txt

add_executable(
  ${TARGET_NAME}
  src/main.c
  src/cli.c
  src/protocol.c
  src/test_runner.c
  src/tests/test_sdram.c
  src/tests/test_foo.c    # ← добавить
  ...
)

target_link_libraries(
  ${TARGET_NAME} PRIVATE
  ...
  bsp_foo     # ← добавить BSP модуль
)

Шаг 4 — Обновить матрицу тестов

Добавить строку в таблицу в этом README.

Шаблоны для разных типов тестов

Self-тест с инициализацией

static void test_foo_init(void)
{
    bsp_foo_init();
}

static void test_foo_deinit(void)
{
    bsp_foo_deinit();
}

const test_module_t k_test_foo = {
    .id     = "foo",
    .init   = test_foo_init,
    .run    = test_foo_run,
    .deinit = test_foo_deinit,
    ...
};

Интерактивный тест (confirm внутри run)

#include "test_runner.h"    /* test_runner_wait_confirm() */
#include "protocol.h"       /* protocol_send_confirm_request() */

static test_result_t test_foo_run(void)
{
    test_result_t result = { .status = TEST_STATUS_PASS };

    confirm_params_t step = {
        .id         = "foo_step1",
        .prompt     = "Выполните действие и подтвердите",
        .timeout_ms = 15000U,
    };

    if (!test_runner_wait_confirm(&step))
    {
        /* таймаут или отказ */
        result.status = TEST_STATUS_SKIP;
        (void) snprintf(result.detail, TEST_DETAIL_SIZE, "%s", "foo_step1 not confirmed");
        return result;
    }

    /* продолжаем тест */
    return result;
}

Тест с pre_confirm (вставить карту, подключить кабель)

const test_module_t k_test_foo = {
    .id                 = "foo",
    .pre_confirm_prompt = "Подключите кабель к разъёму X и нажмите OK",
    .run                = test_foo_run,
    ...
};
/* test_runner сам отправит confirm_request перед вызовом run() */

Host unit-тесты

Фреймворк модулей firmware_test покрыт host unit-тестами (Unity + fff). Тесты компилируются clang-17 на хосте без ARM-специфики.

Что покрыто

Таргет Что тестирует Тест-файл
test_protocol сериализация JSON (все event types) tests/host/protocol/test_protocol.c
test_cli парсинг входящих строк, диспатч по type tests/host/cli/test_cli.c
test_firmware_runner state machine (IDLE/PRE_CONFIRM/RUNNING), реестр tests/host/runner/test_firmware_runner.c

Запуск

# Все host-тесты
just build::test-host

# Только один тест (вербозный вывод Unity)
ctest --preset host-debug-test -R test_cli -V

# Напрямую
./build/host-debug/tests/host/test_cli

Добавление host-теста для нового тест-модуля

Host unit-тесты для test_sdram.c и подобных — опциональны. Тест-модули проверяются через HIL pytest (tools/hil/). Если в тест-модуле есть нетривиальная логика (парсинг результатов, конечный автомат, retry) — стоит добавить host-тест.

Гайд: docs/testing/host/HOST_CREATE_TEST.md.

Моки и UNIT_TEST seam

test_runner.c компилируется с -DUNIT_TEST — это открывает seam для подстановки тестового реестра без изменения production-кода:

/* В тест-файле предоставляем свои модули */
const test_module_t *g_unit_test_registry[8];
size_t               g_unit_test_registry_size = 0U;

static void set_registry(const test_module_t **pp_mods, size_t count) { ... }

void test_run_all_critical_fail_skips_remaining(void)
{
    const test_module_t *mods[] = { &K_MOD_PASS, &K_MOD_CRIT_FAIL, &K_MOD_PASS };
    set_registry(mods, 3U);
    test_runner_run_all();
    /* assertions... */
}

Стандартный список моков для каждого теста:

Зависимость fff fake
cli_send() FAKE_VOID_FUNC(cli_send, const char *) + custom_fake с копией
bsp_tick_get_ms() FAKE_VALUE_FUNC(uint32_t, bsp_tick_get_ms)
bsp_usb_cdc_poll() FAKE_VOID_FUNC(bsp_usb_cdc_poll)
cli_process() FAKE_VOID_FUNC(cli_process)
protocol_send_test_result() custom_fake — копируем *p_result по значению

Ловушка dangling pointer: protocol_send_test_result получает указатель на стековую переменную внутри execute_test(). После возврата указатель инвалиден — используй custom_fake с s_captured = *p_result пока стек жив.


Версионирование

FIRMWARE_TEST_VERSION в protocol.h — единственная точка правды о версии. Поле "fw" в session_start несёт эту строку.

При несовместимых изменениях протокола (новое обязательное поле, изменение семантики) — bumping версии + обновление этого документа.

Хост должен сверять "fw" при подключении и предупреждать оператора при несовпадении ожидаемой версии.


Архитектурные решения (закрыты)

Не пересматривать без явного запроса.

Решение Обоснование
Единственный транспорт — USB CDC ACM HIL ELF-прошивки используют отдельный канал (UART + bsp_uart_host)
Парсинг JSON — strstr без cJSON Схема фиксирована, cJSON избыточен
SDRAM тест — через firmware_test, не HIL ELF Тест идёт командами по USB CDC
IR и RTC — не реализуются Вне scope рекламационной диагностики
Тесты атомарны Инженер сам решает что проверять
Тест-логика на таргете Хост — тонкий клиент, нет дублирования логики