From 73b20d9f3e879f41242b8d38b15d6b8b86c2768a Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Thu, 25 Jun 2026 12:28:00 +0300 Subject: [PATCH] # firmware_test: can & opto done --- bsp/can/src/can.c | 6 + bsp/opto/README.md | 7 +- bsp/opto/include/bsp/opto.h | 10 ++ bsp/opto/src/opto.c | 15 +++ firmware/test/CMakeLists.txt | 4 +- firmware/test/PLAN.md | 91 ++++++++----- firmware/test/src/main.c | 7 +- firmware/test/src/test_runner.c | 1 + firmware/test/src/tests/README.md | 198 ++++++++++++++++++++++++---- firmware/test/src/tests/test_can.c | 14 +- firmware/test/src/tests/test_opto.c | 3 +- tools/hil/06_test_firmware_can.py | 16 ++- tools/hil/06_test_firmware_opto.py | 12 +- 13 files changed, 303 insertions(+), 81 deletions(-) diff --git a/bsp/can/src/can.c b/bsp/can/src/can.c index 25468fc..59c2aa9 100644 --- a/bsp/can/src/can.c +++ b/bsp/can/src/can.c @@ -259,6 +259,12 @@ bsp_status_t bsp_can_init(const bsp_can_config_t *p_config) (void) memcpy(&flexcan_cfg.timingConfig, &timing_cfg, sizeof(timing_cfg)); + /* Workaround ERRATA 50235: FLEXCAN_Init() содержит assert который проверяет + * что CCM_CCGR5_CG12 (LPUART clock gate) открыт когда CAN тактируется + * от осциллятора. Открываем gate и оставляем открытым — закрывать не нужно, + * LPUART1 тактируется с минимальным потреблением. */ + CLOCK_EnableClock(kCLOCK_Lpuart1); + FLEXCAN_Init(BSP_CAN_BASE, &flexcan_cfg, BSP_CAN_CLK_FREQ_HZ); /* MB1 — рабочий TX. */ diff --git a/bsp/opto/README.md b/bsp/opto/README.md index 1872dff..8591318 100644 --- a/bsp/opto/README.md +++ b/bsp/opto/README.md @@ -63,10 +63,15 @@ bsp_status_t bsp_opto_init(const bsp_opto_config_t *p_cfg); void bsp_opto_process(void); /* вызывать из main loop */ bsp_opto_state_t bsp_opto_read(bsp_opto_ch_t ch); bsp_status_t bsp_opto_proto_arm(bsp_opto_ch_t ch); +bsp_opto_state_t bsp_opto_force_read(bsp_opto_ch_t ch); ``` -`bsp_opto_read()` всегда возвращает `BSP_OPTO_STATE_INACTIVE` для каналов +- `bsp_opto_read()` всегда возвращает `BSP_OPTO_STATE_INACTIVE` для каналов в `MODE_PROTO` — используй `GPIO_PinRead` напрямую при побитовом сэмплировании. +- `bsp_opto_force_read()` синхронно читает пин напрямую, обновляет +`confirmed_state` и сбрасывает `pending`. Используется в тестах после +гарантированной стабилизации сигнала — когда дебаунс уже отработал, +но `confirmed_state` мог не обновиться из-за чётного числа ISR при дребезге реле. --- diff --git a/bsp/opto/include/bsp/opto.h b/bsp/opto/include/bsp/opto.h index dedcff8..d671a1f 100644 --- a/bsp/opto/include/bsp/opto.h +++ b/bsp/opto/include/bsp/opto.h @@ -214,6 +214,16 @@ extern "C" */ void bsp_opto_proto_arm(bsp_opto_ch_t ch); + /** + * @brief Прочитать мгновенное состояние пина канала напрямую (без дебаунса). + * + * Используется после гарантированной стабилизации сигнала для синхронного + * чтения в тестах. Обновляет confirmed_state. + * + * @param ch канал (MODE_LEVEL) + * @return текущее состояние пина + */ + bsp_opto_state_t bsp_opto_force_read(bsp_opto_ch_t ch); #ifdef __cplusplus } #endif \ No newline at end of file diff --git a/bsp/opto/src/opto.c b/bsp/opto/src/opto.c index 4ead2f4..d9f51e4 100644 --- a/bsp/opto/src/opto.c +++ b/bsp/opto/src/opto.c @@ -313,4 +313,19 @@ void bsp_opto_process(void) } } } +} + +bsp_opto_state_t bsp_opto_force_read(bsp_opto_ch_t ch) +{ + if (ch >= BSP_OPTO_CH_COUNT || !g_s_opto.channels[ch].enabled || + g_s_opto.channels[ch].mode == BSP_OPTO_MODE_PROTO) + { + return BSP_OPTO_STATE_INACTIVE; + } + + /* Принудительно перечитать пин и обновить confirmed_state. */ + bsp_opto_state_t state = OPTO_PIN_TO_STATE(read_pin(g_s_opto.channels[ch].pin)); + g_s_opto.channels[ch].confirmed_state = state; + g_s_opto.channels[ch].pending = false; + return state; } \ No newline at end of file diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index 8ed7628..09959e7 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -67,8 +67,8 @@ target_link_options( -Wl,--gc-sections -Wl,--print-memory-usage -Wl,-Map=${CMAKE_BINARY_DIR}/firmware_test.map - -Wl,--defsym=__stack_size__=0x1000 - -Wl,--defsym=__heap_size__=0x1000 + -Wl,--defsym=__stack_size__=0x2000 + -Wl,--defsym=__heap_size__=0x2000 -T${PROJECT_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_flexspi_nor_sdram.ld) set_target_properties(${TARGET_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY diff --git a/firmware/test/PLAN.md b/firmware/test/PLAN.md index 79db34e..8608440 100644 --- a/firmware/test/PLAN.md +++ b/firmware/test/PLAN.md @@ -1,6 +1,6 @@ # firmware_test — План разработки -> Версия: 0.6 | Обновлён после завершения Этапа 5 (display + buttons) и архитектурных решений по Этапам 6–8. +> Версия: 0.7 | Обновлён после завершения Этапов 6а–6д, 6е, 6ж (протокол, test_opto, test_can, HIL pytest firmware_cdc). --- @@ -19,24 +19,26 @@ ## Текущий статус -| Компонент | Статус | Примечание | -| ------------------------------ | ------ | -------------------------------------------- | -| `bsp_usb_cdc` | ✅ | HIL тест пройден | -| firmware_test скелет | ✅ | `main.c` + `cli.c` | -| Протокол v2 + test_runner | ✅ | JSON-lines event-driven | -| `bsp_sdram` + `test_sdram` | ✅ | 4 фазы: addr/data/seq/retention | -| `bsp_qspi_flash` + `test_qspi` | ✅ | JEDEC + erase + rw + addr range | -| `bsp_sd` + `test_usd` | ✅ | bsp_sd + FatFS, pre_confirm, 4 шага | -| `bsp_display` + `test_display` | ✅ | 4 цвета + ротация, hardware-verified | -| `bsp_button` + `test_buttons` | ✅ | 2 кнопки, physical detect, hardware-verified | -| Протокол: `list_tests` | ⬜ | Этап 6а | -| Протокол: `run_selected` | ⬜ | Этап 6а | -| `test_opto` | ⬜ | Этап 6б (bsp_opto ✅) | -| `test_can` | ⬜ | Этап 6в (bsp_can ✅) | -| `bsp_mqs` + `test_mqs` | ⬜ | Этап 6г | -| HIL pytest firmware_cdc | ⬜ | Этап 6д | -| Provisioning | ⬜ | Этап 7 | -| TUI сервисного инженера | ⬜ | Этап 8 | +| Компонент | Статус | Примечание | +| ------------------------------ | ------ | ------------------------------------------------ | +| `bsp_usb_cdc` | ✅ | HIL тест пройден | +| firmware_test скелет | ✅ | `main.c` + `cli.c` | +| Протокол v2 + test_runner | ✅ | JSON-lines event-driven | +| `bsp_sdram` + `test_sdram` | ✅ | 4 фазы: addr/data/seq/retention | +| `bsp_qspi_flash` + `test_qspi` | ✅ | JEDEC + erase + rw + addr range | +| `bsp_sd` + `test_usd` | ✅ | bsp_sd + FatFS, pre_confirm, 4 шага | +| `bsp_display` + `test_display` | ✅ | 4 цвета + ротация, hardware-verified | +| `bsp_button` + `test_buttons` | ✅ | 2 кнопки, physical detect, hardware-verified | +| Протокол: `list_tests` | ✅ | Этап 6а, hardware-verified | +| Протокол: `run_selected` | ✅ | Этап 6а, hardware-verified | +| `test_opto` | ✅ | Этап 6б, hardware-verified | +| `test_can` | ✅ | Этап 6в, hardware-verified | +| HIL pytest firmware_cdc | ✅ | Этап 6д, `FirmwareCdc` + `firmware_cdc` фикстура | +| HIL pytest firmware_opto | ✅ | Этап 6е, `06_test_firmware_opto.py` | +| HIL pytest firmware_can | ✅ | Этап 6ж, `06_test_firmware_can.py` | +| `bsp_mqs` + `test_mqs` | ⬜ | Этап 6г | +| Provisioning | ⬜ | Этап 7 | +| TUI сервисного инженера | ⬜ | Этап 8 | --- @@ -50,8 +52,8 @@ | `display` | TFT Display RGB888 | ❌ | ❌ | interactive | `bsp_display` ✅ | ✅ | | `buttons` | Test Buttons 1/2 | ❌ | ❌ | interactive | `bsp_button` ✅ | ✅ | | `mqs` | MQS Audio Out | ❌ | ❌ | interactive | `bsp_mqs` (⬜ новый) | ⬜ | -| `can` | CAN loopback | ❌ | ✅ | HIL | `bsp_can` ✅ | ⬜ | -| `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | HIL | `bsp_opto` ✅ | ⬜ | +| `can` | CAN loopback | ❌ | ✅ | HIL | `bsp_can` ✅ | ✅ | +| `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | HIL | `bsp_opto` ✅ | ✅ | **Убранные тесты (закрытые решения):** @@ -93,6 +95,23 @@ один канал. Буфер всегда стерео (L+R идентичны). - **MQS тест:** захардкоженная мелодия 3–5 с, `confirm_request("mqs_tone")`, оператор слышит → OK/FAIL. `critical=false`, `requires_hil=false`. +- **ERRATA 50235 (FlexCAN + USB):** `FLEXCAN_Init()` содержит assert на + `CCM_CCGR5_CG12` (LPUART clock gate). После `bsp_usb_cdc_init()` gate + может быть закрыт → assert → HardFault. Workaround: `CLOCK_EnableClock(kCLOCK_Lpuart1)` + перед `FLEXCAN_Init()` внутри `bsp_can_init()`. Gate оставляется открытым — + закрывать не нужно, LPUART1 тактируется с минимальным потреблением. + `bsp_can_init()` вызывается из `main()` после `bsp_usb_cdc_init()`. +- **`firmware_cdc` фикстура:** не ждёт `session_start` (одноразовое событие при + старте, может быть пропущено). Проверяет живость через `ping → pong`. +- **`bsp_opto_force_read()`:** добавлен в BSP API для синхронного чтения пина + без дебаунса. Обновляет `confirmed_state`, сбрасывает `pending`. Используется + в `test_opto.c` после settle — обходит race condition когда чётное число ISR + при дребезге реле оставляет `pending=false` с устаревшим `confirmed_state`. +- **`bsp_opto_process()` в `test_opto.c`:** вызывается в settle loop после confirm, + но не в `test_runner_wait_confirm()`. Финальное чтение — через `bsp_opto_force_read()`. +- **Оркестратор оpto:** `RELAY_ON_S=0.15`, `RELAY_OFF_S=0.5` в `06_test_firmware_opto.py`. + Фиксированный sleep достаточен — реле переключается до отправки `confirmed:true`, + `bsp_opto_force_read()` читает финальное состояние пина напрямую. ### Этап 8 (TUI решения) @@ -431,14 +450,14 @@ just host::service-flash # прошить без TUI (для автом ✅ Этап 3 bsp_qspi_flash + test_qspi ✅ Этап 4 bsp_sd + test_usd ✅ Этап 5 display + buttons +✅ Этап 6а протокол: list_tests + run_selected +✅ Этап 6б test_opto.c + hardware верификация +✅ Этап 6в test_can.c + hardware верификация +✅ Этап 6д HIL pytest: firmware_cdc фикстура (FirmwareCdc + firmware_cdc) +✅ Этап 6е HIL pytest: 06_test_firmware_opto.py +✅ Этап 6ж HIL pytest: 06_test_firmware_can.py -⬜ Этап 6а протокол: list_tests + run_selected ← СЛЕДУЮЩИЙ ШАГ -⬜ Этап 6б test_opto.c + hardware верификация -⬜ Этап 6в test_can.c + hardware верификация -⬜ Этап 6г bsp_mqs + test_mqs.c (после получения наработок) -⬜ Этап 6д HIL pytest: firmware_cdc фикстура -⬜ Этап 6е HIL pytest: 06_test_firmware_opto.py -⬜ Этап 6ж HIL pytest: 06_test_firmware_can.py +⬜ Этап 6г bsp_mqs + test_mqs.c ← СЛЕДУЮЩИЙ ШАГ ⬜ Этап 7 Provisioning (OCOTP UID + Flash-флаг) @@ -456,11 +475,11 @@ just host::service-flash # прошить без TUI (для автом ## Зависимости между этапами ```bash -6а (протокол) → 6б (opto) → 6в (can) → 6г (mqs) - ↓ - 6д (conftest) → 6е (opto pytest) → 6ж (can pytest) - ↓ - 7 (provisioning) - ↓ - 8 (TUI) -``` \ No newline at end of file +✅ 6а (протокол) → ✅ 6б (opto) → ✅ 6в (can) → ⬜ 6г (mqs) + ↓ + ✅ 6д (conftest) → ✅ 6е (opto pytest) → ✅ 6ж (can pytest) + ↓ + ⬜ 7 (provisioning) + ↓ + ⬜ 8 (TUI) +``` diff --git a/firmware/test/src/main.c b/firmware/test/src/main.c index e7c981d..1f2e929 100644 --- a/firmware/test/src/main.c +++ b/firmware/test/src/main.c @@ -18,6 +18,7 @@ * 8. Главный цикл — poll + cli_process */ #include "board.h" +#include "bsp/can.h" #include "bsp/led.h" #include "bsp/tick.h" #include "bsp/usb_cdc.h" @@ -29,8 +30,10 @@ int main(void) { - const uint32_t CONNECT_BLINK_MS = 200U; - const uint32_t ERROR_BLINK_MS = 250; + const uint32_t CONNECT_BLINK_MS = 200U; + const uint32_t ERROR_BLINK_MS = 250; + static const bsp_can_config_t K_CAN_CFG = { .bitrate = 125000U }; + board_hw_init(); bsp_led_init(); diff --git a/firmware/test/src/test_runner.c b/firmware/test/src/test_runner.c index 43bd63f..1f2326a 100644 --- a/firmware/test/src/test_runner.c +++ b/firmware/test/src/test_runner.c @@ -430,6 +430,7 @@ bool test_runner_wait_confirm(const confirm_params_t *p_params) { return false; /* таймаут */ } + bsp_usb_cdc_poll(); cli_process(); } diff --git a/firmware/test/src/tests/README.md b/firmware/test/src/tests/README.md index d1842ca..b87c127 100644 --- a/firmware/test/src/tests/README.md +++ b/firmware/test/src/tests/README.md @@ -12,25 +12,28 @@ ### Входящие сообщения (host → target) -| Тип | Пример | Описание | -| --------- | ------------------------------------------------ | --------------------------- | -| `cmd` | `{"type":"cmd","cmd":"ping"}` | Проверка канала | -| `cmd` | `{"type":"cmd","cmd":"run","id":"sdram"}` | Запустить один тест по ID | -| `cmd` | `{"type":"cmd","cmd":"run_all"}` | Запустить все тесты реестра | -| `confirm` | `{"type":"confirm","id":"usd","confirmed":true}` | Ответ оператора на запрос | +| Тип | Пример | Описание | +| --------- | -------------------------------------------------------------- | ------------------------------------ | +| `cmd` | `{"type":"cmd","cmd":"ping"}` | Проверка канала | +| `cmd` | `{"type":"cmd","cmd":"run","id":"sdram"}` | Запустить один тест по ID | +| `cmd` | `{"type":"cmd","cmd":"run_all"}` | Запустить все тесты реестра | +| `cmd` | `{"type":"cmd","cmd":"list_tests"}` | Получить реестр тестов с метаданными | +| `cmd` | `{"type":"cmd","cmd":"run_selected","tests":["sdram","qspi"]}` | Запустить подмножество тестов | +| `confirm` | `{"type":"confirm","id":"usd","confirmed":true}` | Ответ оператора на запрос | ### Исходящие события (target → host) -| Тип | Ключевые поля | Описание | -| ---------------------------- | ---------------------------------------- | ------------------------ | -| `session_start` | `fw`, `target`, `uptime_ms` | Прошивка готова к работе | -| `test_begin` | `id`, `name`, `critical` | Тест запущен | -| `test_result` | `id`, `status`, `ms`, `detail` | Результат теста | -| `confirm_request` | `id`, `prompt`, `timeout_ms` | Запрос оператору | -| `progress` | `test`, `step`, `status` | Прогресс внутри теста | -| `summary` | `passed`, `failed`, `skipped`, `overall` | Итог `run_all` | -| `pong` | — | Ответ на `ping` | -| `{"ok":false,"error":"..."}` | `error` | Ошибка протокола | +| Тип | Ключевые поля | Описание | +| ---------------------------- | -------------------------------------------- | ------------------------------- | +| `session_start` | `fw`, `target`, `uptime_ms` | Прошивка готова к работе | +| `test_list` | `tests[]` (id, name, critical, requires_hil) | Ответ на `list_tests` | +| `test_begin` | `id`, `name`, `critical` | Тест запущен | +| `test_result` | `id`, `status`, `ms`, `detail` | Результат теста | +| `confirm_request` | `id`, `prompt`, `timeout_ms` | Запрос оператору | +| `progress` | `test`, `step`, `status` | Прогресс внутри теста | +| `summary` | `passed`, `failed`, `skipped`, `overall` | Итог `run_all` / `run_selected` | +| `pong` | — | Ответ на `ping` | +| `{"ok":false,"error":"..."}` | `error` | Ошибка протокола | **Возможные статусы `test_result`:** `pass` / `fail` / `skip` @@ -408,13 +411,15 @@ sequenceDiagram ## Реестр тестов — порядок выполнения -| № | ID | Название | Critical | Тип | -| --- | --------- | ------------------ | -------- | ---------------------------- | -| 1 | `sdram` | SDRAM 32 MB | ✅ | Self-test | -| 2 | `qspi` | QSPI Flash W25Qxx | ✅ | Self-test | -| 3 | `usd` | microSD (SDIO) | ❌ | Interactive (pre-confirm) | -| 4 | `display` | TFT Display RGB888 | ❌ | Interactive (in-run confirm) | -| 5 | `buttons` | Test Buttons | ❌ | Interactive (physical) | +| № | ID | Название | Critical | HIL | Тип | +| --- | --------- | ------------------ | -------- | --- | ---------------------------- | +| 1 | `sdram` | SDRAM 32 MB | ✅ | ❌ | Self-test | +| 2 | `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | Self-test | +| 3 | `usd` | microSD (SDIO) | ❌ | ❌ | Interactive (pre-confirm) | +| 4 | `display` | TFT Display RGB888 | ❌ | ❌ | Interactive (in-run confirm) | +| 5 | `buttons` | Test Buttons | ❌ | ❌ | Interactive (physical) | +| 6 | `opto` | Opto Inputs | ❌ | ✅ | HIL (M5StampPLC RLY2/3/4) | +| 7 | `can` | CAN loopback | ❌ | ✅ | HIL (M5StampPLC CAN) | --- @@ -437,6 +442,151 @@ sequenceDiagram | `display` | ` not confirmed` | Оператор не подтвердил / истёк таймаут 15 с | | `buttons` | `btn1_press timeout` | Test_But_1 не нажата за 10 с | | `buttons` | `btn2_press timeout` | Test_But_2 не нажата за 10 с | +| `opto` | ` mismatch: expected ACTIVE got INACTIVE` | Реле не переключило оптовход | +| `can` | `can_rx_ready: no frame received` | M5 не отправил фрейм / CAN не подключён | +| `can` | `rx id mismatch: expected 0x100 got 0x...` | Неверный ID принятого фрейма | +| `can` | `rx data mismatch: got XX XX XX XX` | Данные фрейма не совпадают | +| `can` | `tx failed: bsp_can_send returned ` | TX timeout или шина недоступна | +| `can` | `can_tx_verify: M5 did not confirm tx frame` | M5 не получил фрейм от таргета | | любой | `confirm timeout` | pre-confirm не получен за 30 с | | любой | `operator declined` | Получен `"confirmed":false` | -| любой | `critical test failed` | Предшествующий критичный тест провалился | \ No newline at end of file +| любой | `critical test failed` | Предшествующий критичный тест провалился | + +--- + +## list_tests — получить реестр тестов + +```bash +→ {"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":"opto","name":"Opto Inputs","critical":false,"requires_hil":true}, + {"id":"can","name":"CAN loopback","critical":false,"requires_hil":true} + ]} +``` + +TUI использует этот ответ для динамического построения списка тестов. +HIL-тесты (`requires_hil=true`) недоступны если M5StampPLC не подключён. + +--- + +## run_selected — запустить подмножество тестов + +```bash +→ {"type":"cmd","cmd":"run_selected","tests":["sdram","opto"]} +← {"type":"test_begin","id":"sdram","name":"SDRAM 32 MB","critical":true} +← {"type":"test_result","id":"sdram","status":"pass","ms":15304,"detail":""} +← {"type":"test_begin","id":"opto","name":"Opto Inputs","critical":false} + ...confirm цикл 6 шагов (HIL)... +← {"type":"test_result","id":"opto","status":"pass","ms":3210,"detail":""} +← {"type":"summary","passed":2,"failed":0,"skipped":0,"overall":"pass"} +``` + +Порядок выполнения — как в реестре таргета, не как в запросе. +Если хотя бы один ID не найден — вся команда отклоняется: + +```bash +→ {"type":"cmd","cmd":"run_selected","tests":["sdram","unknown_test"]} +← {"ok":false,"error":"UNKNOWN_TEST"} +``` + +--- + +## Оптоизолированные входы (HIL) + +| Параметр | Значение | +| ---------------- | -------------------------------- | +| ID | `opto` | +| Критичный | ❌ Нет | +| HIL | ✅ Да — M5StampPLC RLY2/RLY3/RLY4 | +| Тип | HIL (автоматический оркестратор) | +| Время выполнения | ~3–5 с (6 шагов) | + +| Шаг | confirm_request id | M5 действие | Проверка | +| --- | ------------------- | ----------- | ----------------------------- | +| 1 | `opto_in1_active` | RLY3 ON | `BSP_OPTO_CH_IN1 == ACTIVE` | +| 2 | `opto_in1_inactive` | RLY3 OFF | `BSP_OPTO_CH_IN1 == INACTIVE` | +| 3 | `opto_in2_active` | RLY4 ON | `BSP_OPTO_CH_IN2 == ACTIVE` | +| 4 | `opto_in2_inactive` | RLY4 OFF | `BSP_OPTO_CH_IN2 == INACTIVE` | +| 5 | `opto_rs_active` | RLY2 ON | `BSP_OPTO_CH_RS == ACTIVE` | +| 6 | `opto_rs_inactive` | RLY2 OFF | `BSP_OPTO_CH_RS == INACTIVE` | + +HIL confirm полностью автоматический — TUI командует M5 и отправляет confirm +без участия оператора. + +```bash +→ {"type":"cmd","cmd":"run","id":"opto"} +← {"type":"test_begin","id":"opto","name":"Opto Inputs","critical":false} +← {"type":"confirm_request","id":"opto_in1_active","prompt":"M5: RLY3 ON -> IN1 ACTIVE","timeout_ms":30000} +→ {"type":"confirm","id":"opto_in1_active","confirmed":true} + ... 5 аналогичных шагов ... +← {"type":"test_result","id":"opto","status":"pass","ms":3210,"detail":""} +``` + +--- + +## CAN loopback (HIL) + +| Параметр | Значение | +| ---------------- | -------------------------------- | +| ID | `can` | +| Критичный | ❌ Нет | +| HIL | ✅ Да — M5StampPLC CAN (SIT1044) | +| Тип | HIL (автоматический оркестратор) | +| Битрейт | 125 kbit/s | +| Время выполнения | ~1–2 с (2 шага) | + +**Шаг 1 — RX (M5 → таргет):** M5 отправляет фрейм `id=0x100 data=[DE AD BE EF]` +до `confirmed:true`. Таргет принимает через `bsp_can_receive()` и верифицирует id + data. + +**Шаг 2 — TX (таргет → M5):** таргет отправляет `id=0x200 data=[CA FE BA BE]` +до `confirm_request`. M5 принимает и верифицирует. TUI отправляет `confirmed:true/false`. + +```bash +→ {"type":"cmd","cmd":"run","id":"can"} +← {"type":"test_begin","id":"can","name":"CAN loopback","critical":false} +← {"type":"confirm_request","id":"can_rx_ready","prompt":"M5: can_send id=0x100 data=[DE AD BE EF]","timeout_ms":30000} +→ {"type":"confirm","id":"can_rx_ready","confirmed":true} +← {"type":"confirm_request","id":"can_tx_verify","prompt":"M5: verify can_recv id=0x200 data=[CA FE BA BE]","timeout_ms":30000} +→ {"type":"confirm","id":"can_tx_verify","confirmed":true} +← {"type":"test_result","id":"can","status":"pass","ms":1240,"detail":""} +``` + +--- + +## HIL pytest — автоматическая верификация через firmware_test CDC + +Два файла тестируют `test_opto` и `test_can` через реальный CDC-протокол v2. +Оркестратор (`FirmwareCdc`) управляет M5 автоматически при каждом `confirm_request`. + +| Файл | Тест | Рецепт Just | +| -------------------------- | ------ | ------------------------------ | +| `06_test_firmware_opto.py` | `opto` | `just host::hil-firmware-opto` | +| `06_test_firmware_can.py` | `can` | `just host::hil-firmware-can` | + +Запуск обоих сразу: + +```bash +just host::hil-firmware +``` + +**Предусловие:** `firmware_test` прошита в Flash и запущена. Порт задаётся +через `HIL_USB_CDC_PORT` в `.env`. Фикстура `firmware_cdc` проверяет живость +через `ping → pong` (не ждёт `session_start` — он отправляется при старте и +может быть пропущен к моменту подключения). + +```bash +just host::hil-firmware-opto +# 06_test_firmware_opto.py::TestFirmwareOpto::test_ping PASSED +# 06_test_firmware_opto.py::TestFirmwareOpto::test_opto_pass PASSED +# 06_test_firmware_opto.py::TestFirmwareOpto::test_opto_in1_fail_on_inactive PASSED + +just host::hil-firmware-can +# 06_test_firmware_can.py::TestFirmwareCan::test_ping PASSED +# 06_test_firmware_can.py::TestFirmwareCan::test_can_pass PASSED +# 06_test_firmware_can.py::TestFirmwareCan::test_can_rx_fail_no_frame PASSED +``` diff --git a/firmware/test/src/tests/test_can.c b/firmware/test/src/tests/test_can.c index 3b51299..2b62673 100644 --- a/firmware/test/src/tests/test_can.c +++ b/firmware/test/src/tests/test_can.c @@ -22,8 +22,6 @@ #include "bsp/can.h" #include "bsp/tick.h" -#include "bsp/usb_cdc.h" -#include "protocol.h" #include "test_module.h" #include "test_runner.h" @@ -65,8 +63,8 @@ static const uint8_t K_TX_DATA[] = { 0xCAU, 0xFEU, 0xBAU, 0xBEU }; */ static void can_init(void) { - const bsp_can_config_t k_cfg = { .bitrate = CAN_BITRATE }; - (void) bsp_can_init(&k_cfg); + const bsp_can_config_t K_CFG = { .bitrate = CAN_BITRATE }; + (void) bsp_can_init(&K_CFG); (void) bsp_can_accept_all(); } @@ -90,13 +88,13 @@ static test_result_t can_run(void) * M5 отправляет фрейм ДО того как подтвердить confirm(true). * После confirm таргет вызывает bsp_can_receive(). */ - const confirm_params_t k_rx_params = { + const confirm_params_t K_RX_PARAMS = { .id = "can_rx_ready", .prompt = "M5: can_send id=0x100 data=[DE AD BE EF]", .timeout_ms = 0U, }; - bool confirmed = test_runner_wait_confirm(&k_rx_params); + bool confirmed = test_runner_wait_confirm(&K_RX_PARAMS); if (!confirmed) { @@ -175,13 +173,13 @@ static test_result_t can_run(void) * Запросить верификацию у TUI: M5 должен был принять наш фрейм. * TUI проверяет id+data и шлёт confirmed(true) или confirmed(false). */ - const confirm_params_t k_tx_params = { + const confirm_params_t K_TX_PARAMS = { .id = "can_tx_verify", .prompt = "M5: verify can_recv id=0x200 data=[CA FE BA BE]", .timeout_ms = 0U, }; - confirmed = test_runner_wait_confirm(&k_tx_params); + confirmed = test_runner_wait_confirm(&K_TX_PARAMS); if (!confirmed) { diff --git a/firmware/test/src/tests/test_opto.c b/firmware/test/src/tests/test_opto.c index c9c713f..858a566 100644 --- a/firmware/test/src/tests/test_opto.c +++ b/firmware/test/src/tests/test_opto.c @@ -10,7 +10,6 @@ #include "bsp/opto.h" #include "bsp/tick.h" #include "bsp/usb_cdc.h" -#include "protocol.h" #include "test_module.h" #include "test_runner.h" @@ -161,7 +160,7 @@ static test_result_t opto_run(void) } /* 4. Верификация */ - bsp_opto_state_t got = bsp_opto_read(p_step->channel); + bsp_opto_state_t got = bsp_opto_force_read(p_step->channel); if (got != p_step->expected_state) { const char *p_exp_str = diff --git a/tools/hil/06_test_firmware_can.py b/tools/hil/06_test_firmware_can.py index 31489af..a91504d 100644 --- a/tools/hil/06_test_firmware_can.py +++ b/tools/hil/06_test_firmware_can.py @@ -37,6 +37,7 @@ class TestFirmwareCan: self.cdc = firmware_cdc self.m5 = m5 + def _on_confirm(self, confirm_id: str) -> bool: """ Оркестратор CAN-теста. @@ -49,9 +50,7 @@ class TestFirmwareCan: Таргет уже отправил фрейм ДО confirm_request. M5 принимает фрейм, проверяет id+data, возвращает результат. """ - import logging - logging.getLogger(__name__).info("on_confirm called: %r", confirm_id) - + if confirm_id == "can_rx_ready": self.m5.can_send(CAN_RX_ID, CAN_RX_DATA) # Небольшая пауза чтобы фрейм успел уйти на шину @@ -80,9 +79,18 @@ class TestFirmwareCan: return False + def test_list_tests(self) -> None: + """Проверить что 'can' есть в реестре таргета.""" + self.cdc.send({"type": "cmd", "cmd": "list_tests"}) + msg = self.cdc.wait_event("test_list", timeout_s=5.0) + ids = [t["id"] for t in msg.get("tests", [])] + print(f"\nЗарегистрированные тесты: {ids}") + assert "can" in ids, f"'can' не найден в реестре: {ids}" + def test_ping(self) -> None: """Базовая проверка CDC-канала.""" self.cdc.ping() + def test_can_pass(self) -> None: """ @@ -92,7 +100,7 @@ class TestFirmwareCan: result = self.cdc.run_hil_test( test_id="can", on_confirm=self._on_confirm, - timeout_s=60.0, + timeout_s=10.0, ) assert result.get("status") == "pass", ( diff --git a/tools/hil/06_test_firmware_opto.py b/tools/hil/06_test_firmware_opto.py index cf96014..7a8979a 100644 --- a/tools/hil/06_test_firmware_opto.py +++ b/tools/hil/06_test_firmware_opto.py @@ -25,8 +25,8 @@ import pytest from conftest import FirmwareCdc, M5Agent -RELAY_ON_S = 0.15 # реле замыкается быстро -RELAY_OFF_S = 0.5 # размыкание + дебаунс прошивки с запасом +RELAY_ON_S = 0.1 # реле замыкается быстро +RELAY_OFF_S = 0.25 # размыкание + дебаунс прошивки с запасом # Маппинг confirm_id → (opto_ch, state) @@ -65,6 +65,14 @@ class TestFirmwareOpto: """Базовая проверка CDC-канала.""" self.cdc.ping() + def test_list_tests(self) -> None: + """Проверить что 'can' есть в реестре таргета.""" + self.cdc.send({"type": "cmd", "cmd": "list_tests"}) + msg = self.cdc.wait_event("test_list", timeout_s=5.0) + ids = [t["id"] for t in msg.get("tests", [])] + print(f"\nЗарегистрированные тесты: {ids}") + assert "can" in ids, f"'can' не найден в реестре: {ids}" + def test_opto_pass(self) -> None: """ Запустить test_opto через firmware_test.