# firmware_test: can & opto done

This commit is contained in:
Dmitry Akimov 2026-06-25 12:28:00 +03:00
parent 20bebf6d5e
commit 73b20d9f3e
13 changed files with 303 additions and 81 deletions

View file

@ -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)); (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); FLEXCAN_Init(BSP_CAN_BASE, &flexcan_cfg, BSP_CAN_CLK_FREQ_HZ);
/* MB1 — рабочий TX. */ /* MB1 — рабочий TX. */

View file

@ -63,10 +63,15 @@ bsp_status_t bsp_opto_init(const bsp_opto_config_t *p_cfg);
void bsp_opto_process(void); /* вызывать из main loop */ void bsp_opto_process(void); /* вызывать из main loop */
bsp_opto_state_t bsp_opto_read(bsp_opto_ch_t ch); 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_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` напрямую при побитовом сэмплировании. в `MODE_PROTO` — используй `GPIO_PinRead` напрямую при побитовом сэмплировании.
- `bsp_opto_force_read()` синхронно читает пин напрямую, обновляет
`confirmed_state` и сбрасывает `pending`. Используется в тестах после
гарантированной стабилизации сигнала — когда дебаунс уже отработал,
но `confirmed_state` мог не обновиться из-за чётного числа ISR при дребезге реле.
--- ---

View file

@ -214,6 +214,16 @@ extern "C"
*/ */
void bsp_opto_proto_arm(bsp_opto_ch_t ch); 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 #ifdef __cplusplus
} }
#endif #endif

View file

@ -314,3 +314,18 @@ 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;
}

View file

@ -67,8 +67,8 @@ target_link_options(
-Wl,--gc-sections -Wl,--gc-sections
-Wl,--print-memory-usage -Wl,--print-memory-usage
-Wl,-Map=${CMAKE_BINARY_DIR}/firmware_test.map -Wl,-Map=${CMAKE_BINARY_DIR}/firmware_test.map
-Wl,--defsym=__stack_size__=0x1000 -Wl,--defsym=__stack_size__=0x2000
-Wl,--defsym=__heap_size__=0x1000 -Wl,--defsym=__heap_size__=0x2000
-T${PROJECT_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_flexspi_nor_sdram.ld) -T${PROJECT_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_flexspi_nor_sdram.ld)
set_target_properties(${TARGET_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY set_target_properties(${TARGET_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY

View file

@ -1,6 +1,6 @@
# firmware_test — План разработки # firmware_test — План разработки
> Версия: 0.6 | Обновлён после завершения Этапа 5 (display + buttons) и архитектурных решений по Этапам 68. > Версия: 0.7 | Обновлён после завершения Этапов 6а6д, 6е, 6ж (протокол, test_opto, test_can, HIL pytest firmware_cdc).
--- ---
@ -20,7 +20,7 @@
## Текущий статус ## Текущий статус
| Компонент | Статус | Примечание | | Компонент | Статус | Примечание |
| ------------------------------ | ------ | -------------------------------------------- | | ------------------------------ | ------ | ------------------------------------------------ |
| `bsp_usb_cdc` | ✅ | HIL тест пройден | | `bsp_usb_cdc` | ✅ | HIL тест пройден |
| firmware_test скелет | ✅ | `main.c` + `cli.c` | | firmware_test скелет | ✅ | `main.c` + `cli.c` |
| Протокол v2 + test_runner | ✅ | JSON-lines event-driven | | Протокол v2 + test_runner | ✅ | JSON-lines event-driven |
@ -29,12 +29,14 @@
| `bsp_sd` + `test_usd` | ✅ | bsp_sd + FatFS, pre_confirm, 4 шага | | `bsp_sd` + `test_usd` | ✅ | bsp_sd + FatFS, pre_confirm, 4 шага |
| `bsp_display` + `test_display` | ✅ | 4 цвета + ротация, hardware-verified | | `bsp_display` + `test_display` | ✅ | 4 цвета + ротация, hardware-verified |
| `bsp_button` + `test_buttons` | ✅ | 2 кнопки, physical detect, hardware-verified | | `bsp_button` + `test_buttons` | ✅ | 2 кнопки, physical detect, hardware-verified |
| Протокол: `list_tests` | ⬜ | Этап 6а | | Протокол: `list_tests` | ✅ | Этап 6а, hardware-verified |
| Протокол: `run_selected` | ⬜ | Этап 6а | | Протокол: `run_selected` | ✅ | Этап 6а, hardware-verified |
| `test_opto` | ⬜ | Этап 6б (bsp_opto ✅) | | `test_opto` | ✅ | Этап 6б, hardware-verified |
| `test_can` | ⬜ | Этап 6в (bsp_can ✅) | | `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г | | `bsp_mqs` + `test_mqs` | ⬜ | Этап 6г |
| HIL pytest firmware_cdc | ⬜ | Этап 6д |
| Provisioning | ⬜ | Этап 7 | | Provisioning | ⬜ | Этап 7 |
| TUI сервисного инженера | ⬜ | Этап 8 | | TUI сервисного инженера | ⬜ | Этап 8 |
@ -50,8 +52,8 @@
| `display` | TFT Display RGB888 | ❌ | ❌ | interactive | `bsp_display` ✅ | ✅ | | `display` | TFT Display RGB888 | ❌ | ❌ | interactive | `bsp_display` ✅ | ✅ |
| `buttons` | Test Buttons 1/2 | ❌ | ❌ | interactive | `bsp_button` ✅ | ✅ | | `buttons` | Test Buttons 1/2 | ❌ | ❌ | interactive | `bsp_button` ✅ | ✅ |
| `mqs` | MQS Audio Out | ❌ | ❌ | interactive | `bsp_mqs` (⬜ новый) | ⬜ | | `mqs` | MQS Audio Out | ❌ | ❌ | interactive | `bsp_mqs` (⬜ новый) | ⬜ |
| `can` | CAN loopback | ❌ | ✅ | HIL | `bsp_can` ✅ | | | `can` | CAN loopback | ❌ | ✅ | HIL | `bsp_can` ✅ | |
| `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | HIL | `bsp_opto` ✅ | | | `opto` | Оптовходы IN1/2+RS | ❌ | ✅ | HIL | `bsp_opto` ✅ | |
**Убранные тесты (закрытые решения):** **Убранные тесты (закрытые решения):**
@ -93,6 +95,23 @@
один канал. Буфер всегда стерео (L+R идентичны). один канал. Буфер всегда стерео (L+R идентичны).
- **MQS тест:** захардкоженная мелодия 35 с, `confirm_request("mqs_tone")`, - **MQS тест:** захардкоженная мелодия 35 с, `confirm_request("mqs_tone")`,
оператор слышит → OK/FAIL. `critical=false`, `requires_hil=false`. оператор слышит → 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 решения) ### Этап 8 (TUI решения)
@ -431,14 +450,14 @@ just host::service-flash <bin> # прошить без TUI (для автом
✅ Этап 3 bsp_qspi_flash + test_qspi ✅ Этап 3 bsp_qspi_flash + test_qspi
✅ Этап 4 bsp_sd + test_usd ✅ Этап 4 bsp_sd + test_usd
✅ Этап 5 display + buttons ✅ Этап 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г bsp_mqs + test_mqs.c ← СЛЕДУЮЩИЙ ШАГ
⬜ Этап 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
⬜ Этап 7 Provisioning (OCOTP UID + Flash-флаг) ⬜ Этап 7 Provisioning (OCOTP UID + Flash-флаг)
@ -456,11 +475,11 @@ just host::service-flash <bin> # прошить без TUI (для автом
## Зависимости между этапами ## Зависимости между этапами
```bash ```bash
6а (протокол) → 6б (opto) → 6в (can) → 6г (mqs) 6а (протокол) → 6б (opto) → 6в (can) → 6г (mqs)
6д (conftest) → 6е (opto pytest) → 6ж (can pytest) 6д (conftest) → 6е (opto pytest) → 6ж (can pytest)
7 (provisioning) 7 (provisioning)
8 (TUI) 8 (TUI)
``` ```

View file

@ -18,6 +18,7 @@
* 8. Главный цикл poll + cli_process * 8. Главный цикл poll + cli_process
*/ */
#include "board.h" #include "board.h"
#include "bsp/can.h"
#include "bsp/led.h" #include "bsp/led.h"
#include "bsp/tick.h" #include "bsp/tick.h"
#include "bsp/usb_cdc.h" #include "bsp/usb_cdc.h"
@ -31,6 +32,8 @@ int main(void)
{ {
const uint32_t CONNECT_BLINK_MS = 200U; const uint32_t CONNECT_BLINK_MS = 200U;
const uint32_t ERROR_BLINK_MS = 250; const uint32_t ERROR_BLINK_MS = 250;
static const bsp_can_config_t K_CAN_CFG = { .bitrate = 125000U };
board_hw_init(); board_hw_init();
bsp_led_init(); bsp_led_init();

View file

@ -430,6 +430,7 @@ bool test_runner_wait_confirm(const confirm_params_t *p_params)
{ {
return false; /* таймаут */ return false; /* таймаут */
} }
bsp_usb_cdc_poll(); bsp_usb_cdc_poll();
cli_process(); cli_process();
} }

View file

@ -13,22 +13,25 @@
### Входящие сообщения (host → target) ### Входящие сообщения (host → target)
| Тип | Пример | Описание | | Тип | Пример | Описание |
| --------- | ------------------------------------------------ | --------------------------- | | --------- | -------------------------------------------------------------- | ------------------------------------ |
| `cmd` | `{"type":"cmd","cmd":"ping"}` | Проверка канала | | `cmd` | `{"type":"cmd","cmd":"ping"}` | Проверка канала |
| `cmd` | `{"type":"cmd","cmd":"run","id":"sdram"}` | Запустить один тест по ID | | `cmd` | `{"type":"cmd","cmd":"run","id":"sdram"}` | Запустить один тест по ID |
| `cmd` | `{"type":"cmd","cmd":"run_all"}` | Запустить все тесты реестра | | `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}` | Ответ оператора на запрос | | `confirm` | `{"type":"confirm","id":"usd","confirmed":true}` | Ответ оператора на запрос |
### Исходящие события (target → host) ### Исходящие события (target → host)
| Тип | Ключевые поля | Описание | | Тип | Ключевые поля | Описание |
| ---------------------------- | ---------------------------------------- | ------------------------ | | ---------------------------- | -------------------------------------------- | ------------------------------- |
| `session_start` | `fw`, `target`, `uptime_ms` | Прошивка готова к работе | | `session_start` | `fw`, `target`, `uptime_ms` | Прошивка готова к работе |
| `test_list` | `tests[]` (id, name, critical, requires_hil) | Ответ на `list_tests` |
| `test_begin` | `id`, `name`, `critical` | Тест запущен | | `test_begin` | `id`, `name`, `critical` | Тест запущен |
| `test_result` | `id`, `status`, `ms`, `detail` | Результат теста | | `test_result` | `id`, `status`, `ms`, `detail` | Результат теста |
| `confirm_request` | `id`, `prompt`, `timeout_ms` | Запрос оператору | | `confirm_request` | `id`, `prompt`, `timeout_ms` | Запрос оператору |
| `progress` | `test`, `step`, `status` | Прогресс внутри теста | | `progress` | `test`, `step`, `status` | Прогресс внутри теста |
| `summary` | `passed`, `failed`, `skipped`, `overall` | Итог `run_all` | | `summary` | `passed`, `failed`, `skipped`, `overall` | Итог `run_all` / `run_selected` |
| `pong` | — | Ответ на `ping` | | `pong` | — | Ответ на `ping` |
| `{"ok":false,"error":"..."}` | `error` | Ошибка протокола | | `{"ok":false,"error":"..."}` | `error` | Ошибка протокола |
@ -408,13 +411,15 @@ sequenceDiagram
## Реестр тестов — порядок выполнения ## Реестр тестов — порядок выполнения
| № | ID | Название | Critical | Тип | | № | ID | Название | Critical | HIL | Тип |
| --- | --------- | ------------------ | -------- | ---------------------------- | | --- | --------- | ------------------ | -------- | --- | ---------------------------- |
| 1 | `sdram` | SDRAM 32 MB | ✅ | Self-test | | 1 | `sdram` | SDRAM 32 MB | ✅ | ❌ | Self-test |
| 2 | `qspi` | QSPI Flash W25Qxx | ✅ | Self-test | | 2 | `qspi` | QSPI Flash W25Qxx | ✅ | ❌ | Self-test |
| 3 | `usd` | microSD (SDIO) | ❌ | Interactive (pre-confirm) | | 3 | `usd` | microSD (SDIO) | ❌ | ❌ | Interactive (pre-confirm) |
| 4 | `display` | TFT Display RGB888 | ❌ | Interactive (in-run confirm) | | 4 | `display` | TFT Display RGB888 | ❌ | ❌ | Interactive (in-run confirm) |
| 5 | `buttons` | Test Buttons | ❌ | Interactive (physical) | | 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` | `<id> not confirmed` | Оператор не подтвердил / истёк таймаут 15 с | | `display` | `<id> not confirmed` | Оператор не подтвердил / истёк таймаут 15 с |
| `buttons` | `btn1_press timeout` | Test_But_1 не нажата за 10 с | | `buttons` | `btn1_press timeout` | Test_But_1 не нажата за 10 с |
| `buttons` | `btn2_press timeout` | Test_But_2 не нажата за 10 с | | `buttons` | `btn2_press timeout` | Test_But_2 не нажата за 10 с |
| `opto` | `<id> 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 <N>` | TX timeout или шина недоступна |
| `can` | `can_tx_verify: M5 did not confirm tx frame` | M5 не получил фрейм от таргета |
| любой | `confirm timeout` | pre-confirm не получен за 30 с | | любой | `confirm timeout` | pre-confirm не получен за 30 с |
| любой | `operator declined` | Получен `"confirmed":false` | | любой | `operator declined` | Получен `"confirmed":false` |
| любой | `critical test failed` | Предшествующий критичный тест провалился | | любой | `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 (автоматический оркестратор) |
| Время выполнения | ~35 с (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 |
| Время выполнения | ~12 с (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
```

View file

@ -22,8 +22,6 @@
#include "bsp/can.h" #include "bsp/can.h"
#include "bsp/tick.h" #include "bsp/tick.h"
#include "bsp/usb_cdc.h"
#include "protocol.h"
#include "test_module.h" #include "test_module.h"
#include "test_runner.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) static void can_init(void)
{ {
const bsp_can_config_t k_cfg = { .bitrate = CAN_BITRATE }; const bsp_can_config_t K_CFG = { .bitrate = CAN_BITRATE };
(void) bsp_can_init(&k_cfg); (void) bsp_can_init(&K_CFG);
(void) bsp_can_accept_all(); (void) bsp_can_accept_all();
} }
@ -90,13 +88,13 @@ static test_result_t can_run(void)
* M5 отправляет фрейм ДО того как подтвердить confirm(true). * M5 отправляет фрейм ДО того как подтвердить confirm(true).
* После confirm таргет вызывает bsp_can_receive(). * После confirm таргет вызывает bsp_can_receive().
*/ */
const confirm_params_t k_rx_params = { const confirm_params_t K_RX_PARAMS = {
.id = "can_rx_ready", .id = "can_rx_ready",
.prompt = "M5: can_send id=0x100 data=[DE AD BE EF]", .prompt = "M5: can_send id=0x100 data=[DE AD BE EF]",
.timeout_ms = 0U, .timeout_ms = 0U,
}; };
bool confirmed = test_runner_wait_confirm(&k_rx_params); bool confirmed = test_runner_wait_confirm(&K_RX_PARAMS);
if (!confirmed) if (!confirmed)
{ {
@ -175,13 +173,13 @@ static test_result_t can_run(void)
* Запросить верификацию у TUI: M5 должен был принять наш фрейм. * Запросить верификацию у TUI: M5 должен был принять наш фрейм.
* TUI проверяет id+data и шлёт confirmed(true) или confirmed(false). * 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", .id = "can_tx_verify",
.prompt = "M5: verify can_recv id=0x200 data=[CA FE BA BE]", .prompt = "M5: verify can_recv id=0x200 data=[CA FE BA BE]",
.timeout_ms = 0U, .timeout_ms = 0U,
}; };
confirmed = test_runner_wait_confirm(&k_tx_params); confirmed = test_runner_wait_confirm(&K_TX_PARAMS);
if (!confirmed) if (!confirmed)
{ {

View file

@ -10,7 +10,6 @@
#include "bsp/opto.h" #include "bsp/opto.h"
#include "bsp/tick.h" #include "bsp/tick.h"
#include "bsp/usb_cdc.h" #include "bsp/usb_cdc.h"
#include "protocol.h"
#include "test_module.h" #include "test_module.h"
#include "test_runner.h" #include "test_runner.h"
@ -161,7 +160,7 @@ static test_result_t opto_run(void)
} }
/* 4. Верификация */ /* 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) if (got != p_step->expected_state)
{ {
const char *p_exp_str = const char *p_exp_str =

View file

@ -37,6 +37,7 @@ class TestFirmwareCan:
self.cdc = firmware_cdc self.cdc = firmware_cdc
self.m5 = m5 self.m5 = m5
def _on_confirm(self, confirm_id: str) -> bool: def _on_confirm(self, confirm_id: str) -> bool:
""" """
Оркестратор CAN-теста. Оркестратор CAN-теста.
@ -49,8 +50,6 @@ class TestFirmwareCan:
Таргет уже отправил фрейм ДО confirm_request. Таргет уже отправил фрейм ДО confirm_request.
M5 принимает фрейм, проверяет id+data, возвращает результат. M5 принимает фрейм, проверяет id+data, возвращает результат.
""" """
import logging
logging.getLogger(__name__).info("on_confirm called: %r", confirm_id)
if confirm_id == "can_rx_ready": if confirm_id == "can_rx_ready":
self.m5.can_send(CAN_RX_ID, CAN_RX_DATA) self.m5.can_send(CAN_RX_ID, CAN_RX_DATA)
@ -80,10 +79,19 @@ class TestFirmwareCan:
return False 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: def test_ping(self) -> None:
"""Базовая проверка CDC-канала.""" """Базовая проверка CDC-канала."""
self.cdc.ping() self.cdc.ping()
def test_can_pass(self) -> None: def test_can_pass(self) -> None:
""" """
Полный прогон test_can: RX + TX через M5. Полный прогон test_can: RX + TX через M5.
@ -92,7 +100,7 @@ class TestFirmwareCan:
result = self.cdc.run_hil_test( result = self.cdc.run_hil_test(
test_id="can", test_id="can",
on_confirm=self._on_confirm, on_confirm=self._on_confirm,
timeout_s=60.0, timeout_s=10.0,
) )
assert result.get("status") == "pass", ( assert result.get("status") == "pass", (

View file

@ -25,8 +25,8 @@ import pytest
from conftest import FirmwareCdc, M5Agent from conftest import FirmwareCdc, M5Agent
RELAY_ON_S = 0.15 # реле замыкается быстро RELAY_ON_S = 0.1 # реле замыкается быстро
RELAY_OFF_S = 0.5 # размыкание + дебаунс прошивки с запасом RELAY_OFF_S = 0.25 # размыкание + дебаунс прошивки с запасом
# Маппинг confirm_id → (opto_ch, state) # Маппинг confirm_id → (opto_ch, state)
@ -65,6 +65,14 @@ class TestFirmwareOpto:
"""Базовая проверка CDC-канала.""" """Базовая проверка CDC-канала."""
self.cdc.ping() 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: def test_opto_pass(self) -> None:
""" """
Запустить test_opto через firmware_test. Запустить test_opto через firmware_test.