# 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

@ -44,7 +44,7 @@
## Транспорт
| Параметр | Значение |
|---|---|
| ------------------------- | ------------------------------------------------------------ |
| Интерфейс | USB CDC ACM |
| Разъём | J2 |
| Кодировка | UTF-8 |
@ -181,7 +181,7 @@
```
| Поле | Тип | Описание |
|---|---|---|
| ----------- | ------ | -------------------------- |
| `fw` | string | Версия firmware_test |
| `target` | string | Идентификатор платформы |
| `uptime_ms` | number | Время с момента старта, мс |
@ -218,7 +218,7 @@
```
| `status` | Смысл |
|---|---|
| -------- | ---------------------------------------------------------------------------- |
| `"pass"` | Тест пройден |
| `"fail"` | Тест провален; поле `detail` содержит описание |
| `"skip"` | Тест пропущен (нет оборудования, таймаут оператора, прерван `critical` fail) |
@ -295,7 +295,7 @@ display (подтвердить цвет), кнопки (нажать кнопк
```
| Код | Причина |
|---|---|
| --------------- | ----------------------------------------------- |
| `PARSE_ERR` | Строка не является валидным JSON-lines запросом |
| `UNKNOWN_CMD` | Поле `"cmd"` содержит неизвестное значение |
| `UNKNOWN_TEST` | Поле `"id"` в `run` не найдено в реестре |
@ -307,7 +307,7 @@ display (подтвердить цвет), кнопки (нажать кнопк
## Матрица тестов
| ID | Название | Тип | Critical | HIL (M5) | Интерактивный |
|---|---|---|---|---|---|
| ---------- | --------------------- | ------------------ | -------- | -------- | ------------------ |
| `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
| `qspi` | QSPI Flash 8 MB | self | ✅ | ❌ | ❌ |
| `usd` | uSD (SDIO) | self + interactive | ❌ | ❌ | ✅ (вставить карту) |
@ -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"}
```
---

View file

@ -19,15 +19,15 @@
## Текущий статус
| Компонент | Статус | Примечание |
|----------------------------|--------|------------|
| -------------------------- | ------ | -------------------------------------- |
| `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 |
| `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 ✅) |
@ -60,7 +60,7 @@
### Аппаратный контекст
| Параметр | Значение |
|---|---|
| ---------------- | -------------------------------------------- |
| Интерфейс | USDHC (SDIO) |
| Карта | microSD, вставляется оператором перед тестом |
| Файловая система | FatFS (SDK middleware) |
@ -88,7 +88,7 @@ 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 мс |
@ -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`.
---
@ -137,7 +142,7 @@ target_link_libraries(bsp_usd
### test_display
| Параметр | Значение |
|---|---|
| -------- | ------------------------------------ |
| Critical | ❌ |
| HIL | ❌ |
| Confirm | Внутри `run()` — 4 отдельных confirm |
@ -151,7 +156,7 @@ target_link_libraries(bsp_usd
### test_buttons
| Параметр | Значение |
|---|---|
| -------- | ------------------------------------- |
| Critical | ❌ |
| HIL | ❌ |
| Confirm | prompt only (детект через bsp_button) |
@ -163,7 +168,7 @@ target_link_libraries(bsp_usd
### Аппаратный контекст кнопок
| Кнопка | Пин MCU | GPIO |
|---|---|---|
| ---------- | ---------- | --------- |
| Test_But_1 | GPIO_B1_14 | GPIO2[30] |
| Test_But_2 | GPIO_B1_15 | GPIO2[31] |
@ -246,11 +251,11 @@ 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` ⬜ | ⬜ |
| `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` ✅ | ⬜ |
@ -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

View file

@ -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.

View file

@ -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) *(запланирован)*
@ -51,7 +51,7 @@
### Аппаратный контекст
| Параметр | Значение |
|---|---|
| --------------- | ------------------------------------------------- |
| Чип | MT48LC16M16A2 |
| Объём | 32 MB |
| Шина данных | 16 бит |
@ -159,7 +159,7 @@ 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 или деградация ячейки |
@ -177,7 +177,7 @@ Address pattern на 256 KB: запись → `flush_dcache` → ожидани
### Типичное время выполнения
| Фаза | ~Время |
|---|---|
| ----------- | --------- |
| Address bus | < 1 мс |
| Data bus | ~1 с |
| Sequential | ~25 с |
@ -195,7 +195,7 @@ Address pattern на 256 KB: запись → `flush_dcache` → ожидани
### Аппаратный контекст
| Параметр | Значение |
|---|---|
| --------------- | ------------------------------------------- |
| Чипы | W25Q64 / W25Q128 / W25Q256 / W25Q512 |
| Объём | 8 / 16 / 32 / 64 MB |
| Интерфейс MCU | FlexSPI1, порт A1 |
@ -335,7 +335,7 @@ 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 | Незнакомая ёмкость, чип не в списке поддержки |
@ -359,7 +359,7 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000
### Типичное время выполнения
| Шаг | Чип | ~Время |
|---|---|---|
| ----------------- | ----------- | ----------- |
| 1: JEDEC ID | все | < 1 мс |
| 2: Erase + Verify | все | ~500 мс |
| 3: Write + Read | все | ~200 мс |
@ -372,28 +372,74 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000
## 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 / D0D3 | GPIO_SD_B0_0005 |
| 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 мс** |
---
@ -408,7 +454,7 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000
Четыре шага с визуальным подтверждением оператора:
| Шаг | ID confirm | Промпт | Таймаут |
|---|---|---|---|
| ------- | --------------- | ---------------------- | ------- |
| Красный | `display_red` | "Экран залит красным?" | 15 с |
| Зелёный | `display_green` | "Экран залит зелёным?" | 15 с |
| Синий | `display_blue` | "Экран залит синим?" | 15 с |
@ -437,7 +483,7 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000
### Аппаратный контекст
| Кнопка | Пин MCU | GPIO |
|---|---|---|
| ---------- | ---------- | --------- |
| Test_But_1 | GPIO_B1_14 | GPIO2[30] |
| Test_But_2 | GPIO_B1_15 | GPIO2[31] |
@ -447,7 +493,7 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000
и детектирует нажатие через `bsp_button` — JSON confirm не нужен.
| Шаг | ID | Промпт | Таймаут |
|---|---|---|---|
| -------- | ------------ | --------------------------- | ------- |
| Кнопка 1 | `btn1_press` | "Нажмите кнопку Test_But_1" | 10 с |
| Кнопка 2 | `btn2_press` | "Нажмите кнопку Test_But_2" | 10 с |
@ -467,7 +513,7 @@ W25Q512: last sector = 0x3FFF000, 0x3FFF000 & 0xFFFFFF = 0xFFF000
### Аппаратный контекст
| Параметр | Значение |
|---|---|
| -------- | ----------------------------------- |
| BSP | `bsp_can` ✅ |
| M5 | CAN интерфейсная плата → шина платы |
@ -491,7 +537,7 @@ M5StampPLC отправляет CAN фрейм → плата принимает
### Аппаратный контекст
| Параметр | Значение |
|---|---|
| -------- | --------------------- |
| BSP | `bsp_uart_host` ✅ |
| M5 | UART ↔ UART TTL платы |
@ -510,7 +556,7 @@ M5 отправляет пакет → плата получает → echo об
### Аппаратный контекст
| Параметр | Значение |
|---|---|
| -------- | --------------------------------------- |
| BSP | `bsp_opto` (rs_as_gpio=true) ✅ |
| Пин MCU | GPIO_AD_B1_07 / GPIO1[23] |
| Оптопара | PS2801-4 (неинвертирующая, active-HIGH) |
@ -537,7 +583,7 @@ M5 RLY2 деактивирует → плата детектирует inactive.
### Аппаратный контекст
| Канал | Пин 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 |

View file

@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
/* ── Константы ─────────────────────────────────────────────────────────── */
/** @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,
};