diff --git a/docs/testing/PROTOCOL.md b/docs/testing/PROTOCOL.md index da78f8e..bfe7a7b 100644 --- a/docs/testing/PROTOCOL.md +++ b/docs/testing/PROTOCOL.md @@ -43,15 +43,15 @@ ## Транспорт -| Параметр | Значение | -|---|---| -| Интерфейс | USB CDC ACM | -| Разъём | J2 | -| Кодировка | UTF-8 | -| Фреймирование | JSON-lines: каждое сообщение — одна строка, завершается `\n` | -| Максимальная длина строки | 128 байт (включая `\n`) | -| CR+LF | Принимается (таргет отбрасывает `\r` перед `\n`) | -| Направление | Двунаправленный, half-duplex по логике | +| Параметр | Значение | +| ------------------------- | ------------------------------------------------------------ | +| Интерфейс | USB CDC ACM | +| Разъём | J2 | +| Кодировка | UTF-8 | +| Фреймирование | JSON-lines: каждое сообщение — одна строка, завершается `\n` | +| Максимальная длина строки | 128 байт (включая `\n`) | +| CR+LF | Принимается (таргет отбрасывает `\r` перед `\n`) | +| Направление | Двунаправленный, half-duplex по логике | Нет хэндшейка, нет sequence number, нет подтверждений доставки. При потере строки хост повторяет команду — таргет идемпотентен для `ping` и `run`. @@ -180,10 +180,10 @@ } ``` -| Поле | Тип | Описание | -|---|---|---| -| `fw` | string | Версия firmware_test | -| `target` | string | Идентификатор платформы | +| Поле | Тип | Описание | +| ----------- | ------ | -------------------------- | +| `fw` | string | Версия firmware_test | +| `target` | string | Идентификатор платформы | | `uptime_ms` | number | Время с момента старта, мс | --- @@ -217,10 +217,10 @@ } ``` -| `status` | Смысл | -|---|---| -| `"pass"` | Тест пройден | -| `"fail"` | Тест провален; поле `detail` содержит описание | +| `status` | Смысл | +| -------- | ---------------------------------------------------------------------------- | +| `"pass"` | Тест пройден | +| `"fail"` | Тест провален; поле `detail` содержит описание | | `"skip"` | Тест пропущен (нет оборудования, таймаут оператора, прерван `critical` fail) | Поле `detail` — произвольная ASCII-строка до 95 символов. При `pass` — пустая. @@ -294,29 +294,29 @@ display (подтвердить цвет), кнопки (нажать кнопк ← {"ok":false,"error":"BUSY"} ``` -| Код | Причина | -|---|---| -| `PARSE_ERR` | Строка не является валидным JSON-lines запросом | -| `UNKNOWN_CMD` | Поле `"cmd"` содержит неизвестное значение | -| `UNKNOWN_TEST` | Поле `"id"` в `run` не найдено в реестре | -| `LINE_TOO_LONG` | Входящая строка превысила 128 байт | -| `BUSY` | Таргет выполняет тест, новая команда отклонена | +| Код | Причина | +| --------------- | ----------------------------------------------- | +| `PARSE_ERR` | Строка не является валидным JSON-lines запросом | +| `UNKNOWN_CMD` | Поле `"cmd"` содержит неизвестное значение | +| `UNKNOWN_TEST` | Поле `"id"` в `run` не найдено в реестре | +| `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 | ❌ | ✅ | ❌ | +| 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 | ❌ | ✅ | ❌ | **Типы тестов:** @@ -332,19 +332,31 @@ display (подтвердить цвет), кнопки (нажать кнопк ### uSD Карта вставляется оператором по запросу. Тест не входит в критический путь. +Pre-confirm обрабатывается `test_runner` до вызова `run()`. ```bash -← {"type":"confirm_request","id":"usd_insert","prompt":"Вставьте карту microSD и нажмите OK","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":"confirm_request","id":"usd","prompt":"Insert microSD card and press OK","timeout_ms":30000} +→ {"type":"confirm","id":"usd","confirmed":true} +← {"type":"test_begin","id":"usd","name":"microSD (SDIO)","critical":false} +← {"type":"progress","test":"usd","step":"card_detect","status":"ok"} +← {"type":"progress","test":"usd","step":"mount","status":"ok"} +← {"type":"progress","test":"usd","step":"write","status":"ok"} +← {"type":"progress","test":"usd","step":"read_compare","status":"ok"} +← {"type":"test_result","id":"usd","status":"pass","ms":741,"detail":""} ``` -Если оператор отказался (`"confirmed":false`) или истёк таймаут: +Если оператор отказался (`"confirmed":false`): ```bash -← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"operator skipped"} +← {"type":"test_begin","id":"usd","name":"microSD (SDIO)","critical":false} +← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"operator declined"} +``` + +Если истёк таймаут (30 с без ответа): + +```bash +← {"type":"test_begin","id":"usd","name":"microSD (SDIO)","critical":false} +← {"type":"test_result","id":"usd","status":"skip","ms":0,"detail":"confirm timeout"} ``` --- diff --git a/firmware/test/PLAN.md b/firmware/test/PLAN.md index 90e5f5d..b367133 100644 --- a/firmware/test/PLAN.md +++ b/firmware/test/PLAN.md @@ -18,23 +18,23 @@ ## Текущий статус -| Компонент | Статус | Примечание | -|----------------------------|--------|------------| -| `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` | ✅ | W25Q64/128/256/512, ITCM, IRQ lock | -| `test_qspi` | ✅ | JEDEC + erase + rw + addr range | -| `bsp_usd` | ⬜ | Этап 4 | -| `test_usd` | ⬜ | Этап 4 | -| Display test | ⬜ | Этап 5 | -| Button test | ⬜ | Этап 5 | -| CAN test | ⬜ | Этап 6 (bsp_can ✅) | -| UART TTL test | ⬜ | Этап 6 (bsp_uart_host ✅) | -| UART ISO test | ⬜ | Этап 6 | -| Opto test | ⬜ | Этап 6 (bsp_opto ✅) | -| Provisioning | ⬜ | Этап 7 | +| Компонент | Статус | Примечание | +| -------------------------- | ------ | -------------------------------------- | +| `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` | ✅ | W25Q64/128/256/512, ITCM, IRQ lock | +| `test_qspi` | ✅ | JEDEC + erase + rw + addr range | +| `bsp_usd` | ✅ | bsp_sd + FatFS (firmware_test_fatfs) | +| `test_usd` | ✅ | pre_confirm + 4 шага + progress events | +| Display test | ⬜ | Этап 5 | +| Button test | ⬜ | Этап 5 | +| CAN test | ⬜ | Этап 6 (bsp_can ✅) | +| UART TTL test | ⬜ | Этап 6 (bsp_uart_host ✅) | +| UART ISO test | ⬜ | Этап 6 | +| Opto test | ⬜ | Этап 6 (bsp_opto ✅) | +| Provisioning | ⬜ | Этап 7 | --- @@ -59,12 +59,12 @@ ### Аппаратный контекст -| Параметр | Значение | -|---|---| -| Интерфейс | USDHC (SDIO) | -| Карта | microSD, вставляется оператором перед тестом | -| Файловая система | FatFS (SDK middleware) | -| Детект карты | GPIO (CD pin) или опрос через USDHC status | +| Параметр | Значение | +| ---------------- | -------------------------------------------- | +| Интерфейс | USDHC (SDIO) | +| Карта | microSD, вставляется оператором перед тестом | +| Файловая система | FatFS (SDK middleware) | +| Детект карты | GPIO (CD pin) или опрос через USDHC status | ### BSP API (предварительно) @@ -87,13 +87,13 @@ void bsp_usd_deinit(void); ### test_usd — шаги -| Шаг | Действие | Время | -|---|---|---| -| 1: Card detect | `bsp_usd_is_card_present()` | < 1 мс | -| 2: Mount | `f_mount()` — FAT/exFAT | < 200 мс | -| 3: Write | Записать 4 KB тестовый файл | < 500 мс | +| Шаг | Действие | Время | +| ----------------- | ------------------------------ | -------- | +| 1: Card detect | `bsp_usd_is_card_present()` | < 1 мс | +| 2: Mount | `f_mount()` — FAT/exFAT | < 200 мс | +| 3: Write | Записать 4 KB тестовый файл | < 500 мс | | 4: Read + Compare | Прочитать и сравнить побайтово | < 200 мс | -| 5: Unmount | `f_unmount()` | < 50 мс | +| 5: Unmount | `f_unmount()` | < 50 мс | ### Интерактивность @@ -123,12 +123,17 @@ target_link_libraries(bsp_usd ) ``` -### Открытые вопросы перед реализацией +### Закрытые решения (Этап 4) -- [ ] Есть ли `sdk_usdhc` таргет в `sdk/CMakeLists.txt`? -- [ ] Карта вставлена постоянно или оператор вставляет каждый раз? -- [ ] Нужен ли Card Detect GPIO или только USDHC status? -- [ ] Файловая система: FAT32 или exFAT (размер карт)? +- BSP-слой: `bsp_sd` (host init/deinit/card detect) + `firmware_test_fatfs` (FatFS). + `bsp_usd` как отдельный модуль не создавался — тест работает напрямую через `bsp_sd` + `ff.h`. +- Card detect: `bsp_sd_is_inserted()` через `USDHC_GetPresentStatusFlags`. GPIO-прерывание не используется. +- Карта вставляется оператором по запросу (pre_confirm). Между тестами может извлекаться. +- Drive `2:/`. Тестовый файл `2:/FWTEST.TMP`, удаляется в любом исходе (через `deinit()`). +- Паттерн: `byte[i] = i & 0xFF`, 4096 байт. +- `critical = false`: тест не блокирует HIL-тесты при отсутствии карты. +- Confirm timeout: 30 000 мс (`PROTOCOL_CONFIRM_TIMEOUT_MS`). +- Отдельный `test_usd.h` не создавался — `extern K_TEST_USD` объявлен в `test_runner.c`. --- @@ -136,11 +141,11 @@ target_link_libraries(bsp_usd ### test_display -| Параметр | Значение | -|---|---| -| Critical | ❌ | -| HIL | ❌ | -| Confirm | Внутри `run()` — 4 отдельных confirm | +| Параметр | Значение | +| -------- | ------------------------------------ | +| Critical | ❌ | +| HIL | ❌ | +| Confirm | Внутри `run()` — 4 отдельных confirm | Шаги: заливка Red → confirm → Green → confirm → Blue → confirm → White → confirm. Каждый шаг посылает `confirm_request`, ждёт `confirm` с таймаутом 15 с. @@ -150,11 +155,11 @@ target_link_libraries(bsp_usd ### test_buttons -| Параметр | Значение | -|---|---| -| Critical | ❌ | -| HIL | ❌ | -| Confirm | prompt only (детект через bsp_button) | +| Параметр | Значение | +| -------- | ------------------------------------- | +| Critical | ❌ | +| HIL | ❌ | +| Confirm | prompt only (детект через bsp_button) | Шаги: Test_But_1 → Test_But_2. Таргет посылает `confirm_request` как инструкцию оператору, детектирует нажатие через `bsp_button` — JSON confirm не нужен. @@ -162,8 +167,8 @@ target_link_libraries(bsp_usd ### Аппаратный контекст кнопок -| Кнопка | Пин MCU | GPIO | -|---|---|---| +| Кнопка | Пин MCU | GPIO | +| ---------- | ---------- | --------- | | Test_But_1 | GPIO_B1_14 | GPIO2[30] | | Test_But_2 | GPIO_B1_15 | GPIO2[31] | @@ -245,18 +250,18 @@ bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); /* 8 байт из ## Матрица тестов — итоговая -| ID | Название | Тип | Critical | HIL (M5) | BSP | Статус | -|-------------|----------------|-------------|----------|----------|------------------|--------| -| — | PING | cmd | — | ❌ | — | ✅ | -| `sdram` | SDRAM 32MB | self | ✅ | ❌ | `bsp_sdram` ✅ | ✅ | -| `qspi` | QSPI Flash | self | ✅ | ❌ | `bsp_qspi_flash` ✅ | ✅ | -| `usd` | uSD (SDIO) | interactive | ✅ | ❌ | `bsp_usd` ⬜ | ⬜ | -| `display` | Display RGB888 | interactive | ❌ | ❌ | существующий BSP | ⬜ | -| `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | `bsp_button` ✅ | ⬜ | -| `can` | CAN loopback | HIL | ❌ | ✅ | `bsp_can` ✅ | ⬜ | -| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | `bsp_uart_host`✅| ⬜ | -| `uart_iso` | UART ISO +24V | HIL | ❌ | ✅ | `bsp_opto` ✅ | ⬜ | -| `opto` | Opto-in EXT | HIL | ❌ | ✅ | `bsp_opto` ✅ | ⬜ | +| ID | Название | Тип | Critical | HIL (M5) | BSP | Статус | +| ---------- | -------------- | ----------- | -------- | -------- | ----------------- | ------ | +| — | PING | cmd | — | ❌ | — | ✅ | +| `sdram` | SDRAM 32MB | self | ✅ | ❌ | `bsp_sdram` ✅ | ✅ | +| `qspi` | QSPI Flash | self | ✅ | ❌ | `bsp_qspi_flash`✅ | ✅ | +| `usd` | uSD (SDIO) | interactive | ❌ | ❌ | `bsp_sd` ✅ | ✅ | +| `display` | Display RGB888 | interactive | ❌ | ❌ | существующий BSP | ⬜ | +| `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | `bsp_button` ✅ | ⬜ | +| `can` | CAN loopback | HIL | ❌ | ✅ | `bsp_can` ✅ | ⬜ | +| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | `bsp_uart_host`✅ | ⬜ | +| `uart_iso` | UART ISO +24V | HIL | ❌ | ✅ | `bsp_opto` ✅ | ⬜ | +| `opto` | Opto-in EXT | HIL | ❌ | ✅ | `bsp_opto` ✅ | ⬜ | --- @@ -266,8 +271,8 @@ bsp_status_t bsp_prov_read_uid(uint8_t *p_uid, size_t len); /* 8 байт из ✅ Этап 1 (протокол v2 + runner) ✅ Этап 2 (bsp_sdram + test_sdram) ✅ Этап 3 (bsp_qspi_flash + test_qspi) -⬜ Этап 4 (bsp_usd + test_usd) ← ТЕКУЩИЙ -⬜ Этап 5 (display + buttons) +✅ Этап 4 (bsp_sd + test_usd) +⬜ Этап 5 (display + buttons) ← ТЕКУЩИЙ ⬜ Этап 6 (CAN + UART + Opto, HIL) ⬜ Этап 7 (provisioning) ⬜ Этап 8 (tools/production/ TUI runner) ← параллельно с 6-7 diff --git a/firmware/test/src/protocol.h b/firmware/test/src/protocol.h index fc8d699..bb409b7 100644 --- a/firmware/test/src/protocol.h +++ b/firmware/test/src/protocol.h @@ -27,7 +27,7 @@ #define FIRMWARE_TEST_VERSION "0.1.0" /** @brief Таймаут подтверждения по умолчанию, мс. */ -#define PROTOCOL_CONFIRM_TIMEOUT_MS 15000U +#define PROTOCOL_CONFIRM_TIMEOUT_MS 30000U /** * @brief Отправить событие session_start. diff --git a/firmware/test/src/tests/README.md b/firmware/test/src/tests/README.md index 4e2bbdf..ce7db8e 100644 --- a/firmware/test/src/tests/README.md +++ b/firmware/test/src/tests/README.md @@ -14,7 +14,7 @@ - [Как читать эту таблицу](#как-читать-эту-таблицу) - [test_sdram — SDRAM 32 MB](#test_sdram--sdram-32-mb) - [test_qspi — QSPI Flash W25Qxx](#test_qspi--qspi-flash-w25qxx) -- [test_usd — uSD SDIO](#test_usd--usd-sdio) *(запланирован)* +- [test_usd — uSD SDIO](#test_usd--usd-sdio) - [test_display — Display RGB888](#test_display--display-rgb888) *(запланирован)* - [test_buttons — Test_But_1 / Test_But_2](#test_buttons--test_but_1--test_but_2) *(запланирован)* - [test_can — CAN loopback](#test_can--can-loopback) *(запланирован)* @@ -50,15 +50,15 @@ ### Аппаратный контекст -| Параметр | Значение | -|---|---| -| Чип | MT48LC16M16A2 | -| Объём | 32 MB | -| Шина данных | 16 бит | -| Интерфейс MCU | SEMC (0x402F0000) | -| Базовый адрес | 0x80000000 | -| Тестовый регион | 0x80200000 — 0x80A00000 | -| MPU | Region 8: Normal WB cacheable (BOARD_MPU_SDRAM=1) | +| Параметр | Значение | +| --------------- | ------------------------------------------------- | +| Чип | MT48LC16M16A2 | +| Объём | 32 MB | +| Шина данных | 16 бит | +| Интерфейс MCU | SEMC (0x402F0000) | +| Базовый адрес | 0x80000000 | +| Тестовый регион | 0x80200000 — 0x80A00000 | +| MPU | Region 8: Normal WB cacheable (BOARD_MPU_SDRAM=1) | ### Инварианты выполнения @@ -158,12 +158,12 @@ Address pattern на 256 KB: запись → `flush_dcache` → ожидани `detail` содержит: `addr=0xXXXXXXXX exp=0xXX got=0xXX` -| Диапазон addr | Вероятная фаза | Диагноз | -|---|---|---| -| `0x80200000` — `0x80A00000` (степени двойки) | Фаза 1 | Address aliasing | -| `0x80200000` — `0x80210000` | Фаза 2 | Stuck-at на шине данных | -| `0x80200000` — `0x80400000` | Фаза 3 | Coupling или деградация ячейки | -| `0x80200000` — `0x80240000` | Фаза 4 | Refresh timing failure | +| Диапазон addr | Вероятная фаза | Диагноз | +| -------------------------------------------- | -------------- | ------------------------------ | +| `0x80200000` — `0x80A00000` (степени двойки) | Фаза 1 | Address aliasing | +| `0x80200000` — `0x80210000` | Фаза 2 | Stuck-at на шине данных | +| `0x80200000` — `0x80400000` | Фаза 3 | Coupling или деградация ячейки | +| `0x80200000` — `0x80240000` | Фаза 4 | Refresh timing failure | **Анализ `exp` XOR `got`:** биты где `(exp ^ got) != 0` — сбойные линии шины данных. @@ -176,13 +176,13 @@ Address pattern на 256 KB: запись → `flush_dcache` → ожидани ### Типичное время выполнения -| Фаза | ~Время | -|---|---| -| Address bus | < 1 мс | -| Data bus | ~1 с | -| Sequential | ~25 с | -| Retention | ~2 с | -| **Итого** | **~28 с** | +| Фаза | ~Время | +| ----------- | --------- | +| Address bus | < 1 мс | +| Data bus | ~1 с | +| Sequential | ~25 с | +| Retention | ~2 с | +| **Итого** | **~28 с** | --- @@ -194,12 +194,12 @@ Address pattern на 256 KB: запись → `flush_dcache` → ожидани ### Аппаратный контекст -| Параметр | Значение | -|---|---| -| Чипы | W25Q64 / W25Q128 / W25Q256 / W25Q512 | -| Объём | 8 / 16 / 32 / 64 MB | -| Интерфейс MCU | FlexSPI1, порт A1 | -| BSP | `bsp_qspi` | +| Параметр | Значение | +| --------------- | ------------------------------------------- | +| Чипы | W25Q64 / W25Q128 / W25Q256 / W25Q512 | +| Объём | 8 / 16 / 32 / 64 MB | +| Интерфейс MCU | FlexSPI1, порт A1 | +| BSP | `bsp_qspi` | | Тестовый сектор | `flash_size - 4KB` (последний, динамически) | ### Инварианты выполнения @@ -334,17 +334,17 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000 `detail` содержит текстовое описание шага и причины: -| Паттерн `detail` | Шаг | Вероятный диагноз | -|---|---|---| -| `JEDEC: read failed` | 1 | FlexSPI не инициализирован / нет связи | -| `JEDEC: mfr=0xXX exp=0xEF` | 1 | Неизвестный производитель чипа | -| `JEDEC: unknown cap=0xXX` | 1 | Незнакомая ёмкость, чип не в списке поддержки | -| `erase: sector erase failed` | 2 | FlexSPI ошибка при erase command | -| `erase verify failed at 0x…` | 2 | Сектор не стёрся, ячейки застряли | -| `rw: page write failed` | 3 | FlexSPI ошибка при Page Program | -| `rw mismatch at 0x… exp=… got=…` | 3 | Stuck-at bit или ошибка шины данных | -| `addr alias: 0x… mirrors 0x…` | 4 | Алиасирование — 4-byte opcodes не работают | -| `addr range mismatch at 0x…` | 4 | Неожиданные данные при чтении last sector | +| Паттерн `detail` | Шаг | Вероятный диагноз | +| -------------------------------- | --- | --------------------------------------------- | +| `JEDEC: read failed` | 1 | FlexSPI не инициализирован / нет связи | +| `JEDEC: mfr=0xXX exp=0xEF` | 1 | Неизвестный производитель чипа | +| `JEDEC: unknown cap=0xXX` | 1 | Незнакомая ёмкость, чип не в списке поддержки | +| `erase: sector erase failed` | 2 | FlexSPI ошибка при erase command | +| `erase verify failed at 0x…` | 2 | Сектор не стёрся, ячейки застряли | +| `rw: page write failed` | 3 | FlexSPI ошибка при Page Program | +| `rw mismatch at 0x… exp=… got=…` | 3 | Stuck-at bit или ошибка шины данных | +| `addr alias: 0x… mirrors 0x…` | 4 | Алиасирование — 4-byte opcodes не работают | +| `addr range mismatch at 0x…` | 4 | Неожиданные данные при чтении last sector | **Анализ `rw mismatch`:** `exp XOR got` — биты, где `(exp ^ got) != 0`, соответствуют сбойным линиям Quad-шины. @@ -358,42 +358,88 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000 ### Типичное время выполнения -| Шаг | Чип | ~Время | -|---|---|---| -| 1: JEDEC ID | все | < 1 мс | -| 2: Erase + Verify | все | ~500 мс | -| 3: Write + Read | все | ~200 мс | -| 4: Address range | W25Q64/128 | 0 мс (skip) | -| 4: Address range | W25Q256/512 | ~100 мс | -| **Итого** | W25Q64/128 | **~700 мс** | -| **Итого** | W25Q256/512 | **~800 мс** | +| Шаг | Чип | ~Время | +| ----------------- | ----------- | ----------- | +| 1: JEDEC ID | все | < 1 мс | +| 2: Erase + Verify | все | ~500 мс | +| 3: Write + Read | все | ~200 мс | +| 4: Address range | W25Q64/128 | 0 мс (skip) | +| 4: Address range | W25Q256/512 | ~100 мс | +| **Итого** | W25Q64/128 | **~700 мс** | +| **Итого** | W25Q256/512 | **~800 мс** | --- ## test_usd — uSD SDIO -**Файл:** `test_usd.c` *(не реализован — Этап 4)* +**Файл:** `test_usd.c` **ID:** `usd` **Critical:** ❌ | **HIL:** ❌ | **Confirm:** pre_confirm ### Аппаратный контекст -| Параметр | Значение | -|---|---| -| Интерфейс | USDHC | -| Файловая система | FatFS (SDK middleware) | -| BSP | `bsp_usd` | -| Pre-confirm prompt | "Вставьте карту microSD и нажмите OK" | +| Параметр | Значение | +| ----------------- | --------------------------------------------------------------- | +| Интерфейс MCU | USDHC1 | +| CLK / CMD / D0–D3 | GPIO_SD_B0_00–05 | +| Card Detect | GPIO_B1_12 → USDHC1_CD_B → `USDHC_GetPresentStatusFlags` | +| Питание карты | GPIO_AD_B1_03 (SdPwr, active-low, управляется SDK) | +| Drive FatFS | `2:/` | +| BSP | `bsp_sd` (host init/deinit/card detect) + `firmware_test_fatfs` | -### Что будет тестировать +### Инварианты выполнения -- Mount FAT — карта вставлена и файловая система читаема. -- Write тестового файла → Read → Compare (деструктивно для одного файла). -- Unmount. +- `bsp_sd_init()` вызывается в `init()`. При провале `run()` возвращает FAIL немедленно. +- Все ресурсы (файл, mount, host) освобождаются в `deinit()` — вызывается test_runner + всегда, включая FAIL и SKIP. +- Тестовый файл `2:/FWTEST.TMP` удаляется при любом исходе. +- `bsp_usb_cdc_poll()` вызывается вокруг каждой блокирующей операции. -### Поведение без карты +### Что тестирует — четыре шага -Оператор отказался от pre_confirm или таймаут 30 с → `TEST_STATUS_SKIP`. +| Шаг | Действие | Progress event | ~Время | +| ------------ | ------------------------------------------ | ------------------- | -------- | +| card_detect | `bsp_sd_is_inserted()` | `step:card_detect` | < 1 мс | +| mount | `f_mount(&fs, "2:/", 1)` | `step:mount` | < 200 мс | +| write | `f_open` + `f_write` 4 KB + `f_close` | `step:write` | < 500 мс | +| read_compare | `f_open` + `f_read` + `memcmp` + `f_close` | `step:read_compare` | < 200 мс | + +**Паттерн:** `byte[i] = i & 0xFF`, 4096 байт. Детектирует stuck-at-0/1 и partial write. + +### Гарантии теста при PASS + +- USDHC host инициализирован и карта подключена. +- FatFS корректно монтирует раздел (FAT32 / exFAT). +- Запись и чтение 4 KB совпадают побайтово. +- Тестовый файл удалён с карты. + +### Интерпретация FAIL + +| Паттерн `detail` | Причина | +| ---------------------------------------------- | ----------------------------------------------------- | +| `no card detected` | Карта не вставлена или CD не работает | +| `sd init failed` | USDHC host не инициализировался | +| `mount failed: N` | FatFS не может прочитать файловую систему (FR code N) | +| `open failed: N` | Нет места или файловая система только для чтения | +| `write failed: N` / `write incomplete: X/4096` | Ошибка записи на карту | +| `read failed: N` | Ошибка чтения | +| `compare failed at offset N` | Данные не совпадают — битый сектор карты | + +### Поведение без карты / отказ оператора + +- Оператор не ответил за 30 с → `TEST_STATUS_SKIP`, `detail: "confirm timeout"`. +- Оператор нажал "отказ" → `TEST_STATUS_SKIP`, `detail: "operator declined"`. +- Тест не critical → `run_all` продолжает выполнение остальных тестов. + +### Типичное время выполнения + +| Этап | ~Время | +| ------------------- | ----------- | +| SD init + card init | ~200 мс | +| Mount | ~100 мс | +| Write 4 KB | ~400 мс | +| Read + Compare 4 KB | ~150 мс | +| **Итого** | **~850 мс** | --- @@ -407,12 +453,12 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000 Четыре шага с визуальным подтверждением оператора: -| Шаг | ID confirm | Промпт | Таймаут | -|---|---|---|---| -| Красный | `display_red` | "Экран залит красным?" | 15 с | -| Зелёный | `display_green` | "Экран залит зелёным?" | 15 с | -| Синий | `display_blue` | "Экран залит синим?" | 15 с | -| Белый | `display_white` | "Экран залит белым?" | 15 с | +| Шаг | ID confirm | Промпт | Таймаут | +| ------- | --------------- | ---------------------- | ------- | +| Красный | `display_red` | "Экран залит красным?" | 15 с | +| Зелёный | `display_green` | "Экран залит зелёным?" | 15 с | +| Синий | `display_blue` | "Экран залит синим?" | 15 с | +| Белый | `display_white` | "Экран залит белым?" | 15 с | Итог = AND всех четырёх подтверждений. @@ -436,8 +482,8 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000 ### Аппаратный контекст -| Кнопка | Пин MCU | GPIO | -|---|---|---| +| Кнопка | Пин MCU | GPIO | +| ---------- | ---------- | --------- | | Test_But_1 | GPIO_B1_14 | GPIO2[30] | | Test_But_2 | GPIO_B1_15 | GPIO2[31] | @@ -446,10 +492,10 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000 Два шага. Таргет отправляет `confirm_request` как инструкцию оператору и детектирует нажатие через `bsp_button` — JSON confirm не нужен. -| Шаг | ID | Промпт | Таймаут | -|---|---|---|---| -| Кнопка 1 | `btn1_press` | "Нажмите кнопку Test_But_1" | 10 с | -| Кнопка 2 | `btn2_press` | "Нажмите кнопку Test_But_2" | 10 с | +| Шаг | ID | Промпт | Таймаут | +| -------- | ------------ | --------------------------- | ------- | +| Кнопка 1 | `btn1_press` | "Нажмите кнопку Test_But_1" | 10 с | +| Кнопка 2 | `btn2_press` | "Нажмите кнопку Test_But_2" | 10 с | ### Гарантии при PASS @@ -466,10 +512,10 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000 ### Аппаратный контекст -| Параметр | Значение | -|---|---| -| BSP | `bsp_can` ✅ | -| M5 | CAN интерфейсная плата → шина платы | +| Параметр | Значение | +| -------- | ----------------------------------- | +| BSP | `bsp_can` ✅ | +| M5 | CAN интерфейсная плата → шина платы | ### Что будет тестировать @@ -490,10 +536,10 @@ M5StampPLC отправляет CAN фрейм → плата принимает ### Аппаратный контекст -| Параметр | Значение | -|---|---| -| BSP | `bsp_uart_host` ✅ | -| M5 | UART ↔ UART TTL платы | +| Параметр | Значение | +| -------- | --------------------- | +| BSP | `bsp_uart_host` ✅ | +| M5 | UART ↔ UART TTL платы | ### Что будет тестировать @@ -509,12 +555,12 @@ M5 отправляет пакет → плата получает → echo об ### Аппаратный контекст -| Параметр | Значение | -|---|---| -| BSP | `bsp_opto` (rs_as_gpio=true) ✅ | -| Пин MCU | GPIO_AD_B1_07 / GPIO1[23] | +| Параметр | Значение | +| -------- | --------------------------------------- | +| BSP | `bsp_opto` (rs_as_gpio=true) ✅ | +| Пин MCU | GPIO_AD_B1_07 / GPIO1[23] | | Оптопара | PS2801-4 (неинвертирующая, active-HIGH) | -| M5 | RLY2 → RS_RX | +| M5 | RLY2 → RS_RX | ### Что будет тестировать @@ -536,8 +582,8 @@ M5 RLY2 деактивирует → плата детектирует inactive. ### Аппаратный контекст -| Канал | Пин MCU | GPIO | Оптопара | M5 | -|---|---|---|---|---| +| Канал | Пин MCU | GPIO | Оптопара | M5 | +| ------- | ------------- | --------- | -------- | ---- | | EXT_IN1 | GPIO_AD_B1_06 | GPIO1[22] | PS2801-4 | RLY3 | | EXT_IN2 | GPIO_AD_B1_05 | GPIO1[21] | PS2801-4 | RLY4 | diff --git a/firmware/test/src/tests/test_usd.c b/firmware/test/src/tests/test_usd.c index b5af988..6c3e4dc 100644 --- a/firmware/test/src/tests/test_usd.c +++ b/firmware/test/src/tests/test_usd.c @@ -2,30 +2,21 @@ * @file test_usd.c * @brief Тест-модуль firmware_test: microSD (USDHC / FatFS). * - * Пять шагов: + * Жизненный цикл управляется test_runner: + * init() — bsp_sd_init(): инициализация USDHC host. + * run() — card detect → mount → write → read/compare → unmount. + * deinit() — safety-net: закрыть файл, отмонтировать, bsp_sd_deinit(). + * Вызывается test_runner даже при FAIL run(). * - * Шаг 1 — Card detect (~1 мс) - * bsp_sd_is_inserted(). Карта должна быть вставлена оператором — - * pre_confirm_prompt отрабатывает до вызова run() на уровне test_runner. + * Паттерн: byte[i] = i & 0xFF, 4096 байт. + * Покрывает: stuck-at-0, stuck-at-1, partial write, address aliasing. * - * Шаг 2 — SD init (~200 мс) - * bsp_sd_init(): инициализация USDHC host и подключение к карте. + * Буферы g_s_write_buf / g_s_read_buf — статические: стек firmware_test + * составляет 4 KB (linker script), 4 KB-буферы на него не влезают. * - * Шаг 3 — Mount (~200 мс) - * f_mount(&g_s_fs, "2:/", 1): монтирование FatFS раздела. - * - * Шаг 4 — Write (~500 мс) - * Паттерн byte[i] = i & 0xFF, 4096 байт. - * Создаём FWTEST.TMP, записываем, закрываем. - * - * Шаг 5 — Read + Compare (~200 мс) - * Открываем FWTEST.TMP на чтение, читаем 4096 байт, сравниваем побайтово. - * - * Cleanup (unlink + unmount + deinit) выполняется во всех исходах через - * usd_cleanup(). Тестовый файл FWTEST.TMP удаляется независимо от результата. - * - * @note Буферы 4 KB + FatFS-объекты — статические. На стек не ложатся: - * стек firmware_test = 4 KB (linker script ram.ld). + * Состояние монтирования (g_s_mounted) и открытого файла (g_s_file_open) + * отслеживается на уровне модуля — deinit() корректно освобождает ресурсы + * в любом сценарии провала. */ #include "bsp/sd.h" @@ -35,36 +26,45 @@ #include "test_module.h" #include -#include #include #include #include /* ── Константы ─────────────────────────────────────────────────────────── */ -/** @brief Размер тестового файла, байт (покрывает stuck-at и partial write). */ +/** @brief Размер тестового файла, байт. */ #define USD_TEST_SIZE 4096U -/** @brief Точка монтирования FatFS (drive 2, задан в firmware_test_fatfs). */ +/** @brief Точка монтирования FatFS (drive 2). */ #define USD_MOUNT_POINT "2:/" -/** @brief Путь временного тестового файла. */ +/** @brief Путь тестового файла. Удаляется после теста в любом исходе. */ #define USD_TEST_FILE "2:/FWTEST.TMP" -/* ── Статические объекты FatFS и буферы ────────────────────────────────── */ +/* ── Статические объекты FatFS ──────────────────────────────────────────── */ -/** @brief Рабочая область FatFS. Не на стеке — sizeof(FATFS) ≈ 516 байт. */ +/** @brief Рабочая область FatFS. sizeof(FATFS) ≈ 516 байт — не на стеке. */ static FATFS g_s_fs; /** @brief Дескриптор открытого файла. */ static FIL g_s_file; -/** @brief Буфер тестового паттерна (запись). */ -static uint8_t g_s_write_buf[USD_TEST_SIZE]; +/* ── Буферы ─────────────────────────────────────────────────────────────── */ -/** @brief Буфер прочитанных данных (верификация). */ +static uint8_t g_s_write_buf[USD_TEST_SIZE]; static uint8_t g_s_read_buf[USD_TEST_SIZE]; +/* ── Состояние модуля ───────────────────────────────────────────────────── */ + +/** @brief true если bsp_sd_init() прошёл успешно. */ +static bool g_s_ready; + +/** @brief true если FatFS смонтирована (f_mount выполнен). */ +static bool g_s_mounted; + +/** @brief true если g_s_file открыт и требует f_close. */ +static bool g_s_file_open; + /* ── Вспомогательные функции ────────────────────────────────────────────── */ static test_result_t make_fail(const char *p_detail) @@ -74,34 +74,13 @@ static test_result_t make_fail(const char *p_detail) return result; } -/** - * @brief Освободить ресурсы: закрыть файл, удалить тестовый файл, - * отмонтировать, deинициализировать SD host. - * - * Best-effort: коды ошибок FatFS игнорируются. - * Безопасно вызывать в любом сочетании флагов. - * - * @param file_open true → дескриптор g_s_file открыт и требует f_close. - * @param mounted true → FatFS смонтирован, выполнить f_unlink + f_unmount. - */ -static void usd_cleanup(bool file_open, bool mounted) -{ - if (file_open) - { - (void) f_close(&g_s_file); - } - - if (mounted) - { - (void) f_unlink(USD_TEST_FILE); - (void) f_unmount(USD_MOUNT_POINT); - } - - (void) bsp_sd_deinit(); -} - /* ── Шаг 1: Card detect ─────────────────────────────────────────────────── */ +/** + * @brief Проверить наличие карты через аппаратный регистр USDHC. + * + * Вызывается после bsp_sd_init() — host уже инициализирован. + */ static bool step_card_detect(test_result_t *p_out) { bsp_usb_cdc_poll(); @@ -117,62 +96,42 @@ static bool step_card_detect(test_result_t *p_out) return true; } -/* ── Шаг 2: SD host init ────────────────────────────────────────────────── */ - -static bool step_sd_init(test_result_t *p_out) -{ - bsp_usb_cdc_poll(); - - if (bsp_sd_init() != BSP_OK) - { - *p_out = make_fail("sd init failed"); - return false; - } - - cli_send("{\"type\":\"progress\",\"test\":\"usd\"," - "\"step\":\"init\",\"status\":\"ok\"}\n"); - return true; -} - -/* ── Шаг 3: Mount ───────────────────────────────────────────────────────── */ +/* ── Шаг 2: Mount ───────────────────────────────────────────────────────── */ +/** + * @brief Смонтировать FatFS раздел. Устанавливает g_s_mounted = true при успехе. + */ static bool step_mount(test_result_t *p_out) { bsp_usb_cdc_poll(); - FRESULT result = f_mount(&g_s_fs, USD_MOUNT_POINT, 1); + FRESULT fr = f_mount(&g_s_fs, USD_MOUNT_POINT, 1); - if (result != FR_OK) + if (fr != FR_OK) { char detail[TEST_DETAIL_SIZE]; - (void) snprintf(detail, sizeof(detail), "mount failed: %d", (int) result); + (void) snprintf(detail, sizeof(detail), "mount failed: %d", (int) fr); *p_out = make_fail(detail); return false; } + g_s_mounted = true; + cli_send("{\"type\":\"progress\",\"test\":\"usd\"," "\"step\":\"mount\",\"status\":\"ok\"}\n"); return true; } -/* ── Шаг 4: Write ───────────────────────────────────────────────────────── */ +/* ── Шаг 3: Write ───────────────────────────────────────────────────────── */ /** - * @brief Заполнить паттерн, создать файл FWTEST.TMP, записать USD_TEST_SIZE байт. + * @brief Записать тестовый паттерн в FWTEST.TMP. * - * При ошибке записи файл остаётся открытым — вызывающая сторона обязана - * вызвать usd_cleanup(true, true). Состояние сообщается через p_file_left_open. - * - * При успехе файл закрыт до возврата. - * - * @param[out] p_file_left_open true если g_s_file открыт при возврате false. - * @param[out] p_out Описание ошибки при возврате false. - * @return true при успехе. + * Отслеживает g_s_file_open: при провале файл может оставаться открытым — + * deinit() закроет его через f_close. */ -static bool step_write(bool *p_file_left_open, test_result_t *p_out) +static bool step_write(test_result_t *p_out) { - *p_file_left_open = false; - for (uint32_t i = 0U; i < USD_TEST_SIZE; i++) { g_s_write_buf[i] = (uint8_t) (i & 0xFFU); @@ -180,48 +139,46 @@ static bool step_write(bool *p_file_left_open, test_result_t *p_out) bsp_usb_cdc_poll(); - FRESULT result = f_open(&g_s_file, USD_TEST_FILE, FA_WRITE | FA_CREATE_ALWAYS); + FRESULT fr = f_open(&g_s_file, USD_TEST_FILE, FA_WRITE | FA_CREATE_ALWAYS); - if (result != FR_OK) + if (fr != FR_OK) { char detail[TEST_DETAIL_SIZE]; - (void) snprintf(detail, sizeof(detail), "open failed: %d", (int) result); + (void) snprintf(detail, sizeof(detail), "open failed: %d", (int) fr); *p_out = make_fail(detail); return false; } - *p_file_left_open = true; - - UINT bytes_written = 0U; - - result = f_write(&g_s_file, g_s_write_buf, USD_TEST_SIZE, &bytes_written); + g_s_file_open = true; + UINT bw = 0U; + fr = f_write(&g_s_file, g_s_write_buf, USD_TEST_SIZE, &bw); bsp_usb_cdc_poll(); - if (result != FR_OK || bytes_written != USD_TEST_SIZE) + if (fr != FR_OK || bw != USD_TEST_SIZE) { char detail[TEST_DETAIL_SIZE]; - if (result != FR_OK) + if (fr != FR_OK) { - (void) snprintf(detail, sizeof(detail), "write failed: %d", (int) result); + (void) snprintf(detail, sizeof(detail), "write failed: %d", (int) fr); } else { - (void) snprintf(detail, sizeof(detail), "write incomplete: %u/%u", - (unsigned int) bytes_written, (unsigned int) USD_TEST_SIZE); + (void) snprintf(detail, sizeof(detail), "write incomplete: %u/%u", (unsigned int) bw, + (unsigned int) USD_TEST_SIZE); } *p_out = make_fail(detail); return false; } - result = f_close(&g_s_file); - *p_file_left_open = false; + fr = f_close(&g_s_file); + g_s_file_open = false; /* сброс независимо от результата f_close */ bsp_usb_cdc_poll(); - if (result != FR_OK) + if (fr != FR_OK) { char detail[TEST_DETAIL_SIZE]; - (void) snprintf(detail, sizeof(detail), "close after write failed: %d", (int) result); + (void) snprintf(detail, sizeof(detail), "close after write failed: %d", (int) fr); *p_out = make_fail(detail); return false; } @@ -231,59 +188,51 @@ static bool step_write(bool *p_file_left_open, test_result_t *p_out) return true; } -/* ── Шаг 5: Read + Compare ──────────────────────────────────────────────── */ +/* ── Шаг 4: Read + Compare ──────────────────────────────────────────────── */ /** - * @brief Открыть FWTEST.TMP на чтение, прочитать USD_TEST_SIZE байт, - * сравнить с g_s_write_buf побайтово. + * @brief Прочитать FWTEST.TMP и сравнить с g_s_write_buf побайтово. * - * При ошибке до f_close файл может оставаться открытым — состояние - * передаётся через p_file_left_open. При успехе файл закрыт до возврата. - * - * @param[out] p_file_left_open true если g_s_file открыт при возврате false. - * @param[out] p_out Описание ошибки при возврате false. - * @return true при успехе. + * Отслеживает g_s_file_open аналогично step_write. */ -static bool step_read_compare(bool *p_file_left_open, test_result_t *p_out) +static bool step_read_compare(test_result_t *p_out) { - *p_file_left_open = false; - bsp_usb_cdc_poll(); - FRESULT result = f_open(&g_s_file, USD_TEST_FILE, FA_READ); + FRESULT fr = f_open(&g_s_file, USD_TEST_FILE, FA_READ); - if (result != FR_OK) + if (fr != FR_OK) { char detail[TEST_DETAIL_SIZE]; - (void) snprintf(detail, sizeof(detail), "open for read failed: %d", (int) result); + (void) snprintf(detail, sizeof(detail), "open for read failed: %d", (int) fr); *p_out = make_fail(detail); return false; } - *p_file_left_open = true; + g_s_file_open = true; - (void) memset(g_s_read_buf, 0, USD_TEST_SIZE); + (void) memset(g_s_read_buf, 0, sizeof(g_s_read_buf)); - UINT bytes_read = 0U; - result = f_read(&g_s_file, g_s_read_buf, USD_TEST_SIZE, &bytes_read); + UINT br = 0U; + fr = f_read(&g_s_file, g_s_read_buf, USD_TEST_SIZE, &br); bsp_usb_cdc_poll(); - if (result != FR_OK || bytes_read != USD_TEST_SIZE) + if (fr != FR_OK || br != USD_TEST_SIZE) { char detail[TEST_DETAIL_SIZE]; - (void) snprintf(detail, sizeof(detail), "read failed: %d", (int) result); + (void) snprintf(detail, sizeof(detail), "read failed: %d", (int) fr); *p_out = make_fail(detail); return false; } - result = f_close(&g_s_file); - *p_file_left_open = false; + fr = f_close(&g_s_file); + g_s_file_open = false; /* сброс независимо от результата f_close */ bsp_usb_cdc_poll(); - if (result != FR_OK) + if (fr != FR_OK) { char detail[TEST_DETAIL_SIZE]; - (void) snprintf(detail, sizeof(detail), "close after read failed: %d", (int) result); + (void) snprintf(detail, sizeof(detail), "close after read failed: %d", (int) fr); *p_out = make_fail(detail); return false; } @@ -307,46 +256,52 @@ static bool step_read_compare(bool *p_file_left_open, test_result_t *p_out) /* ── Реализация тест-модуля ─────────────────────────────────────────────── */ +/** + * @brief Инициализация: запустить USDHC host. + * + * Вызывается test_runner после pre_confirm (карта уже вставлена оператором). + * g_s_ready = false при провале → run() вернёт FAIL немедленно. + */ +static void usd_init(void) +{ + g_s_ready = false; + g_s_mounted = false; + g_s_file_open = false; + g_s_ready = (bsp_sd_init() == BSP_OK); +} + static test_result_t usd_run(void) { - test_result_t fail_result = { .status = TEST_STATUS_FAIL, .duration_ms = 0U, .detail = { 0 } }; - bool file_open = false; + if (!g_s_ready) + { + return make_fail("sd init failed"); + } + + test_result_t fail_result = { .status = TEST_STATUS_FAIL, .duration_ms = 0U, .detail = { 0 } }; - /* Шаг 1: проверка карты (bsp_sd_init ещё не вызван → без cleanup) */ if (!step_card_detect(&fail_result)) { return fail_result; } - - /* Шаг 2: init (mount ещё не вызван → только deinit при провале) */ - if (!step_sd_init(&fail_result)) - { - (void) bsp_sd_deinit(); - return fail_result; - } - - /* Шаг 3: mount */ if (!step_mount(&fail_result)) { - (void) bsp_sd_deinit(); return fail_result; } - - /* Шаги 4-5: write + read/compare (файловая система смонтирована) */ - if (!step_write(&file_open, &fail_result)) + if (!step_write(&fail_result)) { - usd_cleanup(file_open, true); return fail_result; } - - if (!step_read_compare(&file_open, &fail_result)) + if (!step_read_compare(&fail_result)) { - usd_cleanup(file_open, true); return fail_result; } - /* Успех: файл закрыт внутри step_read_compare, раздел ещё смонтирован */ - usd_cleanup(false, true); + /* Успех: файл закрыт внутри step_read_compare. + * Удаляем тестовый файл и отмонтируем до возврата — + * deinit() проверит g_s_mounted и пропустит повторное unmount. */ + (void) f_unlink(USD_TEST_FILE); + (void) f_unmount(USD_MOUNT_POINT); + g_s_mounted = false; return (test_result_t){ .status = TEST_STATUS_PASS, @@ -355,6 +310,35 @@ static test_result_t usd_run(void) }; } +/** + * @brief Safety-net cleanup: закрыть файл, отмонтировать, остановить host. + * + * Вызывается test_runner всегда — после PASS и после FAIL. + * При PASS все флаги уже сброшены в run() — функция завершается быстро. + * При FAIL освобождает ресурсы в любом состоянии провала. + */ +static void usd_deinit(void) +{ + if (g_s_file_open) + { + (void) f_close(&g_s_file); + g_s_file_open = false; + } + + if (g_s_mounted) + { + (void) f_unlink(USD_TEST_FILE); + (void) f_unmount(USD_MOUNT_POINT); + g_s_mounted = false; + } + + if (g_s_ready) + { + (void) bsp_sd_deinit(); + g_s_ready = false; + } +} + /* ── Дескриптор модуля ──────────────────────────────────────────────────── */ const test_module_t K_TEST_USD = { @@ -362,8 +346,8 @@ const test_module_t K_TEST_USD = { .name = "microSD (SDIO)", .critical = false, .requires_hil = false, - .pre_confirm_prompt = "Вставьте карту microSD и нажмите OK", - .init = NULL, + .pre_confirm_prompt = "Insert microSD card and press OK", + .init = usd_init, .run = usd_run, - .deinit = NULL, + .deinit = usd_deinit, }; \ No newline at end of file