# test_usd: done

This commit is contained in:
Dmitry Akimov 2026-05-07 15:17:47 +03:00
parent 10015af93a
commit da48fbf75d
5 changed files with 396 additions and 349 deletions

View file

@ -43,15 +43,15 @@
## Транспорт ## Транспорт
| Параметр | Значение | | Параметр | Значение |
|---|---| | ------------------------- | ------------------------------------------------------------ |
| Интерфейс | USB CDC ACM | | Интерфейс | USB CDC ACM |
| Разъём | J2 | | Разъём | J2 |
| Кодировка | UTF-8 | | Кодировка | UTF-8 |
| Фреймирование | JSON-lines: каждое сообщение — одна строка, завершается `\n` | | Фреймирование | JSON-lines: каждое сообщение — одна строка, завершается `\n` |
| Максимальная длина строки | 128 байт (включая `\n`) | | Максимальная длина строки | 128 байт (включая `\n`) |
| CR+LF | Принимается (таргет отбрасывает `\r` перед `\n`) | | CR+LF | Принимается (таргет отбрасывает `\r` перед `\n`) |
| Направление | Двунаправленный, half-duplex по логике | | Направление | Двунаправленный, half-duplex по логике |
Нет хэндшейка, нет sequence number, нет подтверждений доставки. При потере Нет хэндшейка, нет sequence number, нет подтверждений доставки. При потере
строки хост повторяет команду — таргет идемпотентен для `ping` и `run`. строки хост повторяет команду — таргет идемпотентен для `ping` и `run`.
@ -180,10 +180,10 @@
} }
``` ```
| Поле | Тип | Описание | | Поле | Тип | Описание |
|---|---|---| | ----------- | ------ | -------------------------- |
| `fw` | string | Версия firmware_test | | `fw` | string | Версия firmware_test |
| `target` | string | Идентификатор платформы | | `target` | string | Идентификатор платформы |
| `uptime_ms` | number | Время с момента старта, мс | | `uptime_ms` | number | Время с момента старта, мс |
--- ---
@ -217,10 +217,10 @@
} }
``` ```
| `status` | Смысл | | `status` | Смысл |
|---|---| | -------- | ---------------------------------------------------------------------------- |
| `"pass"` | Тест пройден | | `"pass"` | Тест пройден |
| `"fail"` | Тест провален; поле `detail` содержит описание | | `"fail"` | Тест провален; поле `detail` содержит описание |
| `"skip"` | Тест пропущен (нет оборудования, таймаут оператора, прерван `critical` fail) | | `"skip"` | Тест пропущен (нет оборудования, таймаут оператора, прерван `critical` fail) |
Поле `detail` — произвольная ASCII-строка до 95 символов. При `pass` — пустая. Поле `detail` — произвольная ASCII-строка до 95 символов. При `pass` — пустая.
@ -294,29 +294,29 @@ display (подтвердить цвет), кнопки (нажать кнопк
← {"ok":false,"error":"BUSY"} ← {"ok":false,"error":"BUSY"}
``` ```
| Код | Причина | | Код | Причина |
|---|---| | --------------- | ----------------------------------------------- |
| `PARSE_ERR` | Строка не является валидным JSON-lines запросом | | `PARSE_ERR` | Строка не является валидным JSON-lines запросом |
| `UNKNOWN_CMD` | Поле `"cmd"` содержит неизвестное значение | | `UNKNOWN_CMD` | Поле `"cmd"` содержит неизвестное значение |
| `UNKNOWN_TEST` | Поле `"id"` в `run` не найдено в реестре | | `UNKNOWN_TEST` | Поле `"id"` в `run` не найдено в реестре |
| `LINE_TOO_LONG` | Входящая строка превысила 128 байт | | `LINE_TOO_LONG` | Входящая строка превысила 128 байт |
| `BUSY` | Таргет выполняет тест, новая команда отклонена | | `BUSY` | Таргет выполняет тест, новая команда отклонена |
--- ---
## Матрица тестов ## Матрица тестов
| ID | Название | Тип | Critical | HIL (M5) | Интерактивный | | ID | Название | Тип | Critical | HIL (M5) | Интерактивный |
|---|---|---|---|---|---| | ---------- | --------------------- | ------------------ | -------- | -------- | ------------------ |
| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ | | `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ | | `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ |
| `usd` | uSD (SDIO) | self + interactive | ❌ | ❌ | ✅ (вставить карту) | | `usd` | uSD (SDIO) | self + interactive | ❌ | ❌ | ✅ (вставить карту) |
| `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ (цвета R/G/B/W) | | `display` | Display RGB888 | interactive | ❌ | ❌ | ✅ (цвета R/G/B/W) |
| `buttons` | Кнопки Test_But_1/2 | interactive | ❌ | ❌ | ✅ (нажать кнопки) | | `buttons` | Кнопки Test_But_1/2 | interactive | ❌ | ❌ | ✅ (нажать кнопки) |
| `can` | CAN | HIL | ❌ | ✅ | ❌ | | `can` | CAN | HIL | ❌ | ✅ | ❌ |
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ | | `uart_ttl` | UART TTL | HIL | ❌ | ✅ | ❌ |
| `uart_iso` | UART ISO / RS_RX Opto | HIL | ❌ | ✅ | ❌ | | `uart_iso` | UART ISO / RS_RX Opto | HIL | ❌ | ✅ | ❌ |
| `opto` | Opto-in EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ | | `opto` | Opto-in EXT_IN1/IN2 | HIL | ❌ | ✅ | ❌ |
**Типы тестов:** **Типы тестов:**
@ -332,19 +332,31 @@ display (подтвердить цвет), кнопки (нажать кнопк
### uSD ### uSD
Карта вставляется оператором по запросу. Тест не входит в критический путь. Карта вставляется оператором по запросу. Тест не входит в критический путь.
Pre-confirm обрабатывается `test_runner` до вызова `run()`.
```bash ```bash
← {"type":"confirm_request","id":"usd_insert","prompt":"Вставьте карту microSD и нажмите OK","timeout_ms":30000} ← {"type":"confirm_request","id":"usd","prompt":"Insert microSD card and press OK","timeout_ms":30000}
→ {"type":"confirm","id":"usd_insert","confirmed":true} → {"type":"confirm","id":"usd","confirmed":true}
← {"type":"test_begin","id":"usd",...} ← {"type":"test_begin","id":"usd","name":"microSD (SDIO)","critical":false}
[запись + верификация блока данных] ← {"type":"progress","test":"usd","step":"card_detect","status":"ok"}
← {"type":"test_result","id":"usd","status":"pass","ms":541,"detail":""} ← {"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 ```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"}
``` ```
--- ---

View file

@ -18,23 +18,23 @@
## Текущий статус ## Текущий статус
| Компонент | Статус | Примечание | | Компонент | Статус | Примечание |
|----------------------------|--------|------------| | -------------------------- | ------ | -------------------------------------- |
| `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 |
| `bsp_sdram` + `test_sdram` | ✅ | 4 фазы: addr/data/seq/retention | | `bsp_sdram` + `test_sdram` | ✅ | 4 фазы: addr/data/seq/retention |
| `bsp_qspi_flash` | ✅ | W25Q64/128/256/512, ITCM, IRQ lock | | `bsp_qspi_flash` | ✅ | W25Q64/128/256/512, ITCM, IRQ lock |
| `test_qspi` | ✅ | JEDEC + erase + rw + addr range | | `test_qspi` | ✅ | JEDEC + erase + rw + addr range |
| `bsp_usd` | ⬜ | Этап 4 | | `bsp_usd` | ✅ | bsp_sd + FatFS (firmware_test_fatfs) |
| `test_usd` | ⬜ | Этап 4 | | `test_usd` | ✅ | pre_confirm + 4 шага + progress events |
| Display test | ⬜ | Этап 5 | | Display test | ⬜ | Этап 5 |
| Button test | ⬜ | Этап 5 | | Button test | ⬜ | Этап 5 |
| CAN test | ⬜ | Этап 6 (bsp_can ✅) | | CAN test | ⬜ | Этап 6 (bsp_can ✅) |
| UART TTL test | ⬜ | Этап 6 (bsp_uart_host ✅) | | UART TTL test | ⬜ | Этап 6 (bsp_uart_host ✅) |
| UART ISO test | ⬜ | Этап 6 | | UART ISO test | ⬜ | Этап 6 |
| Opto test | ⬜ | Этап 6 (bsp_opto ✅) | | Opto test | ⬜ | Этап 6 (bsp_opto ✅) |
| Provisioning | ⬜ | Этап 7 | | Provisioning | ⬜ | Этап 7 |
--- ---
@ -59,12 +59,12 @@
### Аппаратный контекст ### Аппаратный контекст
| Параметр | Значение | | Параметр | Значение |
|---|---| | ---------------- | -------------------------------------------- |
| Интерфейс | USDHC (SDIO) | | Интерфейс | USDHC (SDIO) |
| Карта | microSD, вставляется оператором перед тестом | | Карта | microSD, вставляется оператором перед тестом |
| Файловая система | FatFS (SDK middleware) | | Файловая система | FatFS (SDK middleware) |
| Детект карты | GPIO (CD pin) или опрос через USDHC status | | Детект карты | GPIO (CD pin) или опрос через USDHC status |
### BSP API (предварительно) ### BSP API (предварительно)
@ -87,13 +87,13 @@ void bsp_usd_deinit(void);
### test_usd — шаги ### test_usd — шаги
| Шаг | Действие | Время | | Шаг | Действие | Время |
|---|---|---| | ----------------- | ------------------------------ | -------- |
| 1: Card detect | `bsp_usd_is_card_present()` | < 1 мс | | 1: Card detect | `bsp_usd_is_card_present()` | < 1 мс |
| 2: Mount | `f_mount()` — FAT/exFAT | < 200 мс | | 2: Mount | `f_mount()` — FAT/exFAT | < 200 мс |
| 3: Write | Записать 4 KB тестовый файл | < 500 мс | | 3: Write | Записать 4 KB тестовый файл | < 500 мс |
| 4: Read + Compare | Прочитать и сравнить побайтово | < 200 мс | | 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`? - BSP-слой: `bsp_sd` (host init/deinit/card detect) + `firmware_test_fatfs` (FatFS).
- [ ] Карта вставлена постоянно или оператор вставляет каждый раз? `bsp_usd` как отдельный модуль не создавался — тест работает напрямую через `bsp_sd` + `ff.h`.
- [ ] Нужен ли Card Detect GPIO или только USDHC status? - Card detect: `bsp_sd_is_inserted()` через `USDHC_GetPresentStatusFlags`. GPIO-прерывание не используется.
- [ ] Файловая система: FAT32 или exFAT (размер карт)? - Карта вставляется оператором по запросу (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 ### test_display
| Параметр | Значение | | Параметр | Значение |
|---|---| | -------- | ------------------------------------ |
| Critical | ❌ | | Critical | ❌ |
| HIL | ❌ | | HIL | ❌ |
| Confirm | Внутри `run()` — 4 отдельных confirm | | Confirm | Внутри `run()` — 4 отдельных confirm |
Шаги: заливка Red → confirm → Green → confirm → Blue → confirm → White → confirm. Шаги: заливка Red → confirm → Green → confirm → Blue → confirm → White → confirm.
Каждый шаг посылает `confirm_request`, ждёт `confirm` с таймаутом 15 с. Каждый шаг посылает `confirm_request`, ждёт `confirm` с таймаутом 15 с.
@ -150,11 +155,11 @@ target_link_libraries(bsp_usd
### test_buttons ### test_buttons
| Параметр | Значение | | Параметр | Значение |
|---|---| | -------- | ------------------------------------- |
| Critical | ❌ | | Critical | ❌ |
| HIL | ❌ | | HIL | ❌ |
| Confirm | prompt only (детект через bsp_button) | | Confirm | prompt only (детект через bsp_button) |
Шаги: Test_But_1 → Test_But_2. Таргет посылает `confirm_request` как инструкцию Шаги: Test_But_1 → Test_But_2. Таргет посылает `confirm_request` как инструкцию
оператору, детектирует нажатие через `bsp_button` — JSON confirm не нужен. оператору, детектирует нажатие через `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_1 | GPIO_B1_14 | GPIO2[30] |
| Test_But_2 | GPIO_B1_15 | GPIO2[31] | | 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 | Статус | | ID | Название | Тип | Critical | HIL (M5) | BSP | Статус |
|-------------|----------------|-------------|----------|----------|------------------|--------| | ---------- | -------------- | ----------- | -------- | -------- | ----------------- | ------ |
| — | PING | cmd | — | ❌ | — | ✅ | | — | PING | cmd | — | ❌ | — | ✅ |
| `sdram` | SDRAM 32MB | self | ✅ | ❌ | `bsp_sdram` ✅ | ✅ | | `sdram` | SDRAM 32MB | self | ✅ | ❌ | `bsp_sdram` | ✅ |
| `qspi` | QSPI Flash | self | ✅ | ❌ | `bsp_qspi_flash` ✅ | ✅ | | `qspi` | QSPI Flash | self | ✅ | ❌ | `bsp_qspi_flash`✅ | ✅ |
| `usd` | uSD (SDIO) | interactive | ✅ | ❌ | `bsp_usd` ⬜ | ⬜ | | `usd` | uSD (SDIO) | interactive | ❌ | ❌ | `bsp_sd` ✅ | ✅ |
| `display` | Display RGB888 | interactive | ❌ | ❌ | существующий BSP | ⬜ | | `display` | Display RGB888 | interactive | ❌ | ❌ | существующий BSP | ⬜ |
| `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | `bsp_button` ✅ | ⬜ | | `buttons` | Test_But_1/2 | interactive | ❌ | ❌ | `bsp_button` | ⬜ |
| `can` | CAN loopback | HIL | ❌ | ✅ | `bsp_can` ✅ | ⬜ | | `can` | CAN loopback | HIL | ❌ | ✅ | `bsp_can` | ⬜ |
| `uart_ttl` | UART TTL | HIL | ❌ | ✅ | `bsp_uart_host`✅| ⬜ | | `uart_ttl` | UART TTL | HIL | ❌ | ✅ | `bsp_uart_host` | ⬜ |
| `uart_iso` | UART ISO +24V | HIL | ❌ | ✅ | `bsp_opto` ✅ | ⬜ | | `uart_iso` | UART ISO +24V | HIL | ❌ | ✅ | `bsp_opto` | ⬜ |
| `opto` | Opto-in EXT | 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) ✅ Этап 1 (протокол v2 + runner)
✅ Этап 2 (bsp_sdram + test_sdram) ✅ Этап 2 (bsp_sdram + test_sdram)
✅ Этап 3 (bsp_qspi_flash + test_qspi) ✅ Этап 3 (bsp_qspi_flash + test_qspi)
⬜ Этап 4 (bsp_usd + test_usd) ← ТЕКУЩИЙ ✅ Этап 4 (bsp_sd + test_usd)
⬜ Этап 5 (display + buttons) ⬜ Этап 5 (display + buttons) ← ТЕКУЩИЙ
⬜ Этап 6 (CAN + UART + Opto, HIL) ⬜ Этап 6 (CAN + UART + Opto, HIL)
⬜ Этап 7 (provisioning) ⬜ Этап 7 (provisioning)
⬜ Этап 8 (tools/production/ TUI runner) ← параллельно с 6-7 ⬜ Этап 8 (tools/production/ TUI runner) ← параллельно с 6-7

View file

@ -27,7 +27,7 @@
#define FIRMWARE_TEST_VERSION "0.1.0" #define FIRMWARE_TEST_VERSION "0.1.0"
/** @brief Таймаут подтверждения по умолчанию, мс. */ /** @brief Таймаут подтверждения по умолчанию, мс. */
#define PROTOCOL_CONFIRM_TIMEOUT_MS 15000U #define PROTOCOL_CONFIRM_TIMEOUT_MS 30000U
/** /**
* @brief Отправить событие session_start. * @brief Отправить событие session_start.

View file

@ -14,7 +14,7 @@
- [Как читать эту таблицу](#как-читать-эту-таблицу) - [Как читать эту таблицу](#как-читать-эту-таблицу)
- [test_sdram — SDRAM 32 MB](#test_sdram--sdram-32-mb) - [test_sdram — SDRAM 32 MB](#test_sdram--sdram-32-mb)
- [test_qspi — QSPI Flash W25Qxx](#test_qspi--qspi-flash-w25qxx) - [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_display — Display RGB888](#test_display--display-rgb888) *(запланирован)*
- [test_buttons — Test_But_1 / Test_But_2](#test_buttons--test_but_1--test_but_2) *(запланирован)* - [test_buttons — Test_But_1 / Test_But_2](#test_buttons--test_but_1--test_but_2) *(запланирован)*
- [test_can — CAN loopback](#test_can--can-loopback) *(запланирован)* - [test_can — CAN loopback](#test_can--can-loopback) *(запланирован)*
@ -50,15 +50,15 @@
### Аппаратный контекст ### Аппаратный контекст
| Параметр | Значение | | Параметр | Значение |
|---|---| | --------------- | ------------------------------------------------- |
| Чип | MT48LC16M16A2 | | Чип | MT48LC16M16A2 |
| Объём | 32 MB | | Объём | 32 MB |
| Шина данных | 16 бит | | Шина данных | 16 бит |
| Интерфейс MCU | SEMC (0x402F0000) | | Интерфейс MCU | SEMC (0x402F0000) |
| Базовый адрес | 0x80000000 | | Базовый адрес | 0x80000000 |
| Тестовый регион | 0x80200000 — 0x80A00000 | | Тестовый регион | 0x80200000 — 0x80A00000 |
| MPU | Region 8: Normal WB cacheable (BOARD_MPU_SDRAM=1) | | 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` `detail` содержит: `addr=0xXXXXXXXX exp=0xXX got=0xXX`
| Диапазон addr | Вероятная фаза | Диагноз | | Диапазон addr | Вероятная фаза | Диагноз |
|---|---|---| | -------------------------------------------- | -------------- | ------------------------------ |
| `0x80200000``0x80A00000` (степени двойки) | Фаза 1 | Address aliasing | | `0x80200000``0x80A00000` (степени двойки) | Фаза 1 | Address aliasing |
| `0x80200000``0x80210000` | Фаза 2 | Stuck-at на шине данных | | `0x80200000``0x80210000` | Фаза 2 | Stuck-at на шине данных |
| `0x80200000``0x80400000` | Фаза 3 | Coupling или деградация ячейки | | `0x80200000``0x80400000` | Фаза 3 | Coupling или деградация ячейки |
| `0x80200000``0x80240000` | Фаза 4 | Refresh timing failure | | `0x80200000``0x80240000` | Фаза 4 | Refresh timing failure |
**Анализ `exp` XOR `got`:** биты где `(exp ^ got) != 0` — сбойные линии шины данных. **Анализ `exp` XOR `got`:** биты где `(exp ^ got) != 0` — сбойные линии шины данных.
@ -176,13 +176,13 @@ Address pattern на 256 KB: запись → `flush_dcache` → ожидани
### Типичное время выполнения ### Типичное время выполнения
| Фаза | ~Время | | Фаза | ~Время |
|---|---| | ----------- | --------- |
| Address bus | < 1 мс | | Address bus | < 1 мс |
| Data bus | ~1 с | | Data bus | ~1 с |
| Sequential | ~25 с | | Sequential | ~25 с |
| Retention | ~2 с | | Retention | ~2 с |
| **Итого** | **~28 с** | | **Итого** | **~28 с** |
--- ---
@ -194,12 +194,12 @@ Address pattern на 256 KB: запись → `flush_dcache` → ожидани
### Аппаратный контекст ### Аппаратный контекст
| Параметр | Значение | | Параметр | Значение |
|---|---| | --------------- | ------------------------------------------- |
| Чипы | W25Q64 / W25Q128 / W25Q256 / W25Q512 | | Чипы | W25Q64 / W25Q128 / W25Q256 / W25Q512 |
| Объём | 8 / 16 / 32 / 64 MB | | Объём | 8 / 16 / 32 / 64 MB |
| Интерфейс MCU | FlexSPI1, порт A1 | | Интерфейс MCU | FlexSPI1, порт A1 |
| BSP | `bsp_qspi` | | BSP | `bsp_qspi` |
| Тестовый сектор | `flash_size - 4KB` (последний, динамически) | | Тестовый сектор | `flash_size - 4KB` (последний, динамически) |
### Инварианты выполнения ### Инварианты выполнения
@ -334,17 +334,17 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000
`detail` содержит текстовое описание шага и причины: `detail` содержит текстовое описание шага и причины:
| Паттерн `detail` | Шаг | Вероятный диагноз | | Паттерн `detail` | Шаг | Вероятный диагноз |
|---|---|---| | -------------------------------- | --- | --------------------------------------------- |
| `JEDEC: read failed` | 1 | FlexSPI не инициализирован / нет связи | | `JEDEC: read failed` | 1 | FlexSPI не инициализирован / нет связи |
| `JEDEC: mfr=0xXX exp=0xEF` | 1 | Неизвестный производитель чипа | | `JEDEC: mfr=0xXX exp=0xEF` | 1 | Неизвестный производитель чипа |
| `JEDEC: unknown cap=0xXX` | 1 | Незнакомая ёмкость, чип не в списке поддержки | | `JEDEC: unknown cap=0xXX` | 1 | Незнакомая ёмкость, чип не в списке поддержки |
| `erase: sector erase failed` | 2 | FlexSPI ошибка при erase command | | `erase: sector erase failed` | 2 | FlexSPI ошибка при erase command |
| `erase verify failed at 0x…` | 2 | Сектор не стёрся, ячейки застряли | | `erase verify failed at 0x…` | 2 | Сектор не стёрся, ячейки застряли |
| `rw: page write failed` | 3 | FlexSPI ошибка при Page Program | | `rw: page write failed` | 3 | FlexSPI ошибка при Page Program |
| `rw mismatch at 0x… exp=… got=…` | 3 | Stuck-at bit или ошибка шины данных | | `rw mismatch at 0x… exp=… got=…` | 3 | Stuck-at bit или ошибка шины данных |
| `addr alias: 0x… mirrors 0x…` | 4 | Алиасирование — 4-byte opcodes не работают | | `addr alias: 0x… mirrors 0x…` | 4 | Алиасирование — 4-byte opcodes не работают |
| `addr range mismatch at 0x…` | 4 | Неожиданные данные при чтении last sector | | `addr range mismatch at 0x…` | 4 | Неожиданные данные при чтении last sector |
**Анализ `rw mismatch`:** `exp XOR got` — биты, где `(exp ^ got) != 0`, **Анализ `rw mismatch`:** `exp XOR got` — биты, где `(exp ^ got) != 0`,
соответствуют сбойным линиям Quad-шины. соответствуют сбойным линиям Quad-шины.
@ -358,42 +358,88 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000
### Типичное время выполнения ### Типичное время выполнения
| Шаг | Чип | ~Время | | Шаг | Чип | ~Время |
|---|---|---| | ----------------- | ----------- | ----------- |
| 1: JEDEC ID | все | < 1 мс | | 1: JEDEC ID | все | < 1 мс |
| 2: Erase + Verify | все | ~500 мс | | 2: Erase + Verify | все | ~500 мс |
| 3: Write + Read | все | ~200 мс | | 3: Write + Read | все | ~200 мс |
| 4: Address range | W25Q64/128 | 0 мс (skip) | | 4: Address range | W25Q64/128 | 0 мс (skip) |
| 4: Address range | W25Q256/512 | ~100 мс | | 4: Address range | W25Q256/512 | ~100 мс |
| **Итого** | W25Q64/128 | **~700 мс** | | **Итого** | W25Q64/128 | **~700 мс** |
| **Итого** | W25Q256/512 | **~800 мс** | | **Итого** | W25Q256/512 | **~800 мс** |
--- ---
## test_usd — uSD SDIO ## test_usd — uSD SDIO
**Файл:** `test_usd.c` *(не реализован — Этап 4)* **Файл:** `test_usd.c`
**ID:** `usd` **ID:** `usd`
**Critical:** ❌ | **HIL:** ❌ | **Confirm:** pre_confirm **Critical:** ❌ | **HIL:** ❌ | **Confirm:** pre_confirm
### Аппаратный контекст ### Аппаратный контекст
| Параметр | Значение | | Параметр | Значение |
|---|---| | ----------------- | --------------------------------------------------------------- |
| Интерфейс | USDHC | | Интерфейс MCU | USDHC1 |
| Файловая система | FatFS (SDK middleware) | | CLK / CMD / D0D3 | GPIO_SD_B0_0005 |
| BSP | `bsp_usd` | | Card Detect | GPIO_B1_12 → USDHC1_CD_B → `USDHC_GetPresentStatusFlags` |
| Pre-confirm prompt | "Вставьте карту microSD и нажмите OK" | | Питание карты | GPIO_AD_B1_03 (SdPwr, active-low, управляется SDK) |
| Drive FatFS | `2:/` |
| BSP | `bsp_sd` (host init/deinit/card detect) + `firmware_test_fatfs` |
### Что будет тестировать ### Инварианты выполнения
- Mount FAT — карта вставлена и файловая система читаема. - `bsp_sd_init()` вызывается в `init()`. При провале `run()` возвращает FAIL немедленно.
- Write тестового файла → Read → Compare (деструктивно для одного файла). - Все ресурсы (файл, mount, host) освобождаются в `deinit()` — вызывается test_runner
- Unmount. всегда, включая 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 | Промпт | Таймаут | | Шаг | ID confirm | Промпт | Таймаут |
|---|---|---|---| | ------- | --------------- | ---------------------- | ------- |
| Красный | `display_red` | "Экран залит красным?" | 15 с | | Красный | `display_red` | "Экран залит красным?" | 15 с |
| Зелёный | `display_green` | "Экран залит зелёным?" | 15 с | | Зелёный | `display_green` | "Экран залит зелёным?" | 15 с |
| Синий | `display_blue` | "Экран залит синим?" | 15 с | | Синий | `display_blue` | "Экран залит синим?" | 15 с |
| Белый | `display_white` | "Экран залит белым?" | 15 с | | Белый | `display_white` | "Экран залит белым?" | 15 с |
Итог = AND всех четырёх подтверждений. Итог = 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_1 | GPIO_B1_14 | GPIO2[30] |
| Test_But_2 | GPIO_B1_15 | GPIO2[31] | | Test_But_2 | GPIO_B1_15 | GPIO2[31] |
@ -446,10 +492,10 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000
Два шага. Таргет отправляет `confirm_request` как инструкцию оператору Два шага. Таргет отправляет `confirm_request` как инструкцию оператору
и детектирует нажатие через `bsp_button` — JSON confirm не нужен. и детектирует нажатие через `bsp_button` — JSON confirm не нужен.
| Шаг | ID | Промпт | Таймаут | | Шаг | ID | Промпт | Таймаут |
|---|---|---|---| | -------- | ------------ | --------------------------- | ------- |
| Кнопка 1 | `btn1_press` | "Нажмите кнопку Test_But_1" | 10 с | | Кнопка 1 | `btn1_press` | "Нажмите кнопку Test_But_1" | 10 с |
| Кнопка 2 | `btn2_press` | "Нажмите кнопку Test_But_2" | 10 с | | Кнопка 2 | `btn2_press` | "Нажмите кнопку Test_But_2" | 10 с |
### Гарантии при PASS ### Гарантии при PASS
@ -466,10 +512,10 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000
### Аппаратный контекст ### Аппаратный контекст
| Параметр | Значение | | Параметр | Значение |
|---|---| | -------- | ----------------------------------- |
| BSP | `bsp_can` ✅ | | BSP | `bsp_can` |
| M5 | CAN интерфейсная плата → шина платы | | M5 | CAN интерфейсная плата → шина платы |
### Что будет тестировать ### Что будет тестировать
@ -490,10 +536,10 @@ M5StampPLC отправляет CAN фрейм → плата принимает
### Аппаратный контекст ### Аппаратный контекст
| Параметр | Значение | | Параметр | Значение |
|---|---| | -------- | --------------------- |
| BSP | `bsp_uart_host` ✅ | | BSP | `bsp_uart_host` |
| M5 | UART ↔ UART TTL платы | | M5 | UART ↔ UART TTL платы |
### Что будет тестировать ### Что будет тестировать
@ -509,12 +555,12 @@ M5 отправляет пакет → плата получает → echo об
### Аппаратный контекст ### Аппаратный контекст
| Параметр | Значение | | Параметр | Значение |
|---|---| | -------- | --------------------------------------- |
| BSP | `bsp_opto` (rs_as_gpio=true) ✅ | | BSP | `bsp_opto` (rs_as_gpio=true) ✅ |
| Пин MCU | GPIO_AD_B1_07 / GPIO1[23] | | Пин MCU | GPIO_AD_B1_07 / GPIO1[23] |
| Оптопара | PS2801-4 (неинвертирующая, active-HIGH) | | Оптопара | 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_IN1 | GPIO_AD_B1_06 | GPIO1[22] | PS2801-4 | RLY3 |
| EXT_IN2 | GPIO_AD_B1_05 | GPIO1[21] | PS2801-4 | RLY4 | | EXT_IN2 | GPIO_AD_B1_05 | GPIO1[21] | PS2801-4 | RLY4 |

View file

@ -2,30 +2,21 @@
* @file test_usd.c * @file test_usd.c
* @brief Тест-модуль firmware_test: microSD (USDHC / FatFS). * @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 мс) * Паттерн: byte[i] = i & 0xFF, 4096 байт.
* bsp_sd_is_inserted(). Карта должна быть вставлена оператором * Покрывает: stuck-at-0, stuck-at-1, partial write, address aliasing.
* pre_confirm_prompt отрабатывает до вызова run() на уровне test_runner.
* *
* Шаг 2 SD init (~200 мс) * Буферы g_s_write_buf / g_s_read_buf статические: стек firmware_test
* bsp_sd_init(): инициализация USDHC host и подключение к карте. * составляет 4 KB (linker script), 4 KB-буферы на него не влезают.
* *
* Шаг 3 Mount (~200 мс) * Состояние монтирования (g_s_mounted) и открытого файла (g_s_file_open)
* f_mount(&g_s_fs, "2:/", 1): монтирование FatFS раздела. * отслеживается на уровне модуля deinit() корректно освобождает ресурсы
* * в любом сценарии провала.
* Шаг 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).
*/ */
#include "bsp/sd.h" #include "bsp/sd.h"
@ -35,36 +26,45 @@
#include "test_module.h" #include "test_module.h"
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
/* ── Константы ─────────────────────────────────────────────────────────── */ /* ── Константы ─────────────────────────────────────────────────────────── */
/** @brief Размер тестового файла, байт (покрывает stuck-at и partial write). */ /** @brief Размер тестового файла, байт. */
#define USD_TEST_SIZE 4096U #define USD_TEST_SIZE 4096U
/** @brief Точка монтирования FatFS (drive 2, задан в firmware_test_fatfs). */ /** @brief Точка монтирования FatFS (drive 2). */
#define USD_MOUNT_POINT "2:/" #define USD_MOUNT_POINT "2:/"
/** @brief Путь временного тестового файла. */ /** @brief Путь тестового файла. Удаляется после теста в любом исходе. */
#define USD_TEST_FILE "2:/FWTEST.TMP" #define USD_TEST_FILE "2:/FWTEST.TMP"
/* ── Статические объекты FatFS и буферы ────────────────────────────────── */ /* ── Статические объекты FatFS ──────────────────────────────────────────── */
/** @brief Рабочая область FatFS. Не на стеке — sizeof(FATFS) ≈ 516 байт. */ /** @brief Рабочая область FatFS. sizeof(FATFS) ≈ 516 байт — не на стеке. */
static FATFS g_s_fs; static FATFS g_s_fs;
/** @brief Дескриптор открытого файла. */ /** @brief Дескриптор открытого файла. */
static FIL g_s_file; 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]; 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) 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; 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 ─────────────────────────────────────────────────── */ /* ── Шаг 1: Card detect ─────────────────────────────────────────────────── */
/**
* @brief Проверить наличие карты через аппаратный регистр USDHC.
*
* Вызывается после bsp_sd_init() host уже инициализирован.
*/
static bool step_card_detect(test_result_t *p_out) static bool step_card_detect(test_result_t *p_out)
{ {
bsp_usb_cdc_poll(); bsp_usb_cdc_poll();
@ -117,62 +96,42 @@ static bool step_card_detect(test_result_t *p_out)
return true; return true;
} }
/* ── Шаг 2: SD host init ────────────────────────────────────────────────── */ /* ── Шаг 2: Mount ───────────────────────────────────────────────────────── */
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 ───────────────────────────────────────────────────────── */
/**
* @brief Смонтировать FatFS раздел. Устанавливает g_s_mounted = true при успехе.
*/
static bool step_mount(test_result_t *p_out) static bool step_mount(test_result_t *p_out)
{ {
bsp_usb_cdc_poll(); 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]; 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); *p_out = make_fail(detail);
return false; return false;
} }
g_s_mounted = true;
cli_send("{\"type\":\"progress\",\"test\":\"usd\"," cli_send("{\"type\":\"progress\",\"test\":\"usd\","
"\"step\":\"mount\",\"status\":\"ok\"}\n"); "\"step\":\"mount\",\"status\":\"ok\"}\n");
return true; return true;
} }
/* ── Шаг 4: Write ───────────────────────────────────────────────────────── */ /* ── Шаг 3: Write ───────────────────────────────────────────────────────── */
/** /**
* @brief Заполнить паттерн, создать файл FWTEST.TMP, записать USD_TEST_SIZE байт. * @brief Записать тестовый паттерн в FWTEST.TMP.
* *
* При ошибке записи файл остаётся открытым вызывающая сторона обязана * Отслеживает g_s_file_open: при провале файл может оставаться открытым
* вызвать usd_cleanup(true, true). Состояние сообщается через p_file_left_open. * deinit() закроет его через f_close.
*
* При успехе файл закрыт до возврата.
*
* @param[out] p_file_left_open true если g_s_file открыт при возврате false.
* @param[out] p_out Описание ошибки при возврате false.
* @return true при успехе.
*/ */
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++) for (uint32_t i = 0U; i < USD_TEST_SIZE; i++)
{ {
g_s_write_buf[i] = (uint8_t) (i & 0xFFU); 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(); 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]; 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); *p_out = make_fail(detail);
return false; return false;
} }
*p_file_left_open = true; g_s_file_open = true;
UINT bytes_written = 0U;
result = f_write(&g_s_file, g_s_write_buf, USD_TEST_SIZE, &bytes_written);
UINT bw = 0U;
fr = f_write(&g_s_file, g_s_write_buf, USD_TEST_SIZE, &bw);
bsp_usb_cdc_poll(); 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]; 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 else
{ {
(void) snprintf(detail, sizeof(detail), "write incomplete: %u/%u", (void) snprintf(detail, sizeof(detail), "write incomplete: %u/%u", (unsigned int) bw,
(unsigned int) bytes_written, (unsigned int) USD_TEST_SIZE); (unsigned int) USD_TEST_SIZE);
} }
*p_out = make_fail(detail); *p_out = make_fail(detail);
return false; return false;
} }
result = f_close(&g_s_file); fr = f_close(&g_s_file);
*p_file_left_open = false; g_s_file_open = false; /* сброс независимо от результата f_close */
bsp_usb_cdc_poll(); bsp_usb_cdc_poll();
if (result != FR_OK) if (fr != FR_OK)
{ {
char detail[TEST_DETAIL_SIZE]; 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); *p_out = make_fail(detail);
return false; return false;
} }
@ -231,59 +188,51 @@ static bool step_write(bool *p_file_left_open, test_result_t *p_out)
return true; return true;
} }
/* ── Шаг 5: Read + Compare ──────────────────────────────────────────────── */ /* ── Шаг 4: Read + Compare ──────────────────────────────────────────────── */
/** /**
* @brief Открыть FWTEST.TMP на чтение, прочитать USD_TEST_SIZE байт, * @brief Прочитать FWTEST.TMP и сравнить с g_s_write_buf побайтово.
* сравнить с g_s_write_buf побайтово.
* *
* При ошибке до f_close файл может оставаться открытым состояние * Отслеживает g_s_file_open аналогично step_write.
* передаётся через p_file_left_open. При успехе файл закрыт до возврата.
*
* @param[out] p_file_left_open true если g_s_file открыт при возврате false.
* @param[out] p_out Описание ошибки при возврате false.
* @return true при успехе.
*/ */
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(); 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]; 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); *p_out = make_fail(detail);
return false; 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; UINT br = 0U;
result = f_read(&g_s_file, g_s_read_buf, USD_TEST_SIZE, &bytes_read); fr = f_read(&g_s_file, g_s_read_buf, USD_TEST_SIZE, &br);
bsp_usb_cdc_poll(); 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]; 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); *p_out = make_fail(detail);
return false; return false;
} }
result = f_close(&g_s_file); fr = f_close(&g_s_file);
*p_file_left_open = false; g_s_file_open = false; /* сброс независимо от результата f_close */
bsp_usb_cdc_poll(); bsp_usb_cdc_poll();
if (result != FR_OK) if (fr != FR_OK)
{ {
char detail[TEST_DETAIL_SIZE]; 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); *p_out = make_fail(detail);
return false; 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) static test_result_t usd_run(void)
{ {
test_result_t fail_result = { .status = TEST_STATUS_FAIL, .duration_ms = 0U, .detail = { 0 } }; if (!g_s_ready)
bool file_open = false; {
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)) if (!step_card_detect(&fail_result))
{ {
return 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)) if (!step_mount(&fail_result))
{ {
(void) bsp_sd_deinit();
return fail_result; return fail_result;
} }
if (!step_write(&fail_result))
/* Шаги 4-5: write + read/compare (файловая система смонтирована) */
if (!step_write(&file_open, &fail_result))
{ {
usd_cleanup(file_open, true);
return fail_result; return fail_result;
} }
if (!step_read_compare(&fail_result))
if (!step_read_compare(&file_open, &fail_result))
{ {
usd_cleanup(file_open, true);
return fail_result; return fail_result;
} }
/* Успех: файл закрыт внутри step_read_compare, раздел ещё смонтирован */ /* Успех: файл закрыт внутри step_read_compare.
usd_cleanup(false, true); * Удаляем тестовый файл и отмонтируем до возврата
* 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){ return (test_result_t){
.status = TEST_STATUS_PASS, .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 = { const test_module_t K_TEST_USD = {
@ -362,8 +346,8 @@ const test_module_t K_TEST_USD = {
.name = "microSD (SDIO)", .name = "microSD (SDIO)",
.critical = false, .critical = false,
.requires_hil = false, .requires_hil = false,
.pre_confirm_prompt = "Вставьте карту microSD и нажмите OK", .pre_confirm_prompt = "Insert microSD card and press OK",
.init = NULL, .init = usd_init,
.run = usd_run, .run = usd_run,
.deinit = NULL, .deinit = usd_deinit,
}; };