diff --git a/.gitignore b/.gitignore index d766beb..e8ea310 100644 --- a/.gitignore +++ b/.gitignore @@ -74,4 +74,4 @@ __pycache__/ .claude tools/host/.venv-host/ tools/host/.venv-host-win/ - +.zed/ diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index 9fcebc1..0c11de0 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -13,6 +13,7 @@ add_executable( src/test_runner.c src/tests/test_sdram.c src/tests/test_qspi.c + src/tests/test_usd.c ${BSP_GENERATED}/clock_config.c ${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE}) diff --git a/firmware/test/src/main.c b/firmware/test/src/main.c index efef25f..52503fb 100644 --- a/firmware/test/src/main.c +++ b/firmware/test/src/main.c @@ -19,11 +19,9 @@ */ #include "board.h" #include "bsp/led.h" -#include "bsp/sd.h" #include "bsp/tick.h" #include "bsp/usb_cdc.h" #include "cli.h" -#include "ff.h" #include "protocol.h" #include "test_runner.h" @@ -53,7 +51,6 @@ int main(void) bsp_delay(CONNECT_BLINK_MS); } -#if 0 bsp_led_on(LED_APP); cli_init(); @@ -65,55 +62,4 @@ int main(void) cli_process(); test_runner_process(); } -#endif - static FATFS s_fs; - const char *step = "card_detect"; - bool passed = false; - - if (!bsp_sd_is_inserted()) - { - /* нет карты — пропускаем, это не ошибка стека */ - bsp_usb_cdc_write((const uint8_t *) "The card is not inserted.\r\n", 27); - goto sd_smoke_done; - } - - step = "sd_init"; - if (bsp_sd_init() != BSP_OK) - { - goto sd_smoke_fail; - } - - step = "f_mount"; - FRESULT res = f_mount(&s_fs, "2:/", 1); - if (res != FR_OK) - { - bsp_sd_deinit(); - goto sd_smoke_fail; - } - - /* всё ок */ - res = f_unmount("2:/"); - if (res != FR_OK) - { - bsp_sd_deinit(); - goto sd_smoke_fail; - } - bsp_sd_deinit(); - passed = true; - - bsp_usb_cdc_write((const uint8_t *) "The test passed.\r\n", 18); - goto sd_smoke_done; - -sd_smoke_fail: -{ - /* маленький буфер: step не длиннее 16 символов */ - bsp_usb_cdc_write((const uint8_t *) "The test failed.\r\n", 18); -} - (void) passed; - -sd_smoke_done: - while (1) - { - bsp_usb_cdc_poll(); - } } \ No newline at end of file diff --git a/firmware/test/src/test_runner.c b/firmware/test/src/test_runner.c index 59e8600..39619dd 100644 --- a/firmware/test/src/test_runner.c +++ b/firmware/test/src/test_runner.c @@ -43,11 +43,13 @@ #ifndef UNIT_TEST extern const test_module_t K_TEST_SDRAM; extern const test_module_t K_TEST_QSPI; +extern const test_module_t K_TEST_USD; static const test_module_t *const k_registry[] = { /* populated starting from Этап 2 */ &K_TEST_SDRAM, &K_TEST_QSPI, + &K_TEST_USD, }; #define REGISTRY_SIZE (sizeof(k_registry) / sizeof(k_registry[0])) diff --git a/firmware/test/src/tests/test_usd.c b/firmware/test/src/tests/test_usd.c new file mode 100644 index 0000000..b5af988 --- /dev/null +++ b/firmware/test/src/tests/test_usd.c @@ -0,0 +1,369 @@ +/** + * @file test_usd.c + * @brief Тест-модуль firmware_test: microSD (USDHC / FatFS). + * + * Пять шагов: + * + * Шаг 1 — Card detect (~1 мс) + * bsp_sd_is_inserted(). Карта должна быть вставлена оператором — + * pre_confirm_prompt отрабатывает до вызова run() на уровне test_runner. + * + * Шаг 2 — SD init (~200 мс) + * bsp_sd_init(): инициализация USDHC host и подключение к карте. + * + * Шаг 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). + */ + +#include "bsp/sd.h" +#include "bsp/usb_cdc.h" +#include "cli.h" +#include "ff.h" +#include "test_module.h" + +#include +#include +#include +#include +#include + +/* ── Константы ─────────────────────────────────────────────────────────── */ + +/** @brief Размер тестового файла, байт (покрывает stuck-at и partial write). */ +#define USD_TEST_SIZE 4096U + +/** @brief Точка монтирования FatFS (drive 2, задан в firmware_test_fatfs). */ +#define USD_MOUNT_POINT "2:/" + +/** @brief Путь временного тестового файла. */ +#define USD_TEST_FILE "2:/FWTEST.TMP" + +/* ── Статические объекты FatFS и буферы ────────────────────────────────── */ + +/** @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_read_buf[USD_TEST_SIZE]; + +/* ── Вспомогательные функции ────────────────────────────────────────────── */ + +static test_result_t make_fail(const char *p_detail) +{ + test_result_t result = { .status = TEST_STATUS_FAIL, .duration_ms = 0U }; + (void) snprintf(result.detail, TEST_DETAIL_SIZE, "%s", 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 ─────────────────────────────────────────────────── */ + +static bool step_card_detect(test_result_t *p_out) +{ + bsp_usb_cdc_poll(); + + if (!bsp_sd_is_inserted()) + { + *p_out = make_fail("no card detected"); + return false; + } + + cli_send("{\"type\":\"progress\",\"test\":\"usd\"," + "\"step\":\"card_detect\",\"status\":\"ok\"}\n"); + 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 ───────────────────────────────────────────────────────── */ + +static bool step_mount(test_result_t *p_out) +{ + bsp_usb_cdc_poll(); + + FRESULT result = f_mount(&g_s_fs, USD_MOUNT_POINT, 1); + + if (result != FR_OK) + { + char detail[TEST_DETAIL_SIZE]; + (void) snprintf(detail, sizeof(detail), "mount failed: %d", (int) result); + *p_out = make_fail(detail); + return false; + } + + cli_send("{\"type\":\"progress\",\"test\":\"usd\"," + "\"step\":\"mount\",\"status\":\"ok\"}\n"); + return true; +} + +/* ── Шаг 4: Write ───────────────────────────────────────────────────────── */ + +/** + * @brief Заполнить паттерн, создать файл FWTEST.TMP, записать USD_TEST_SIZE байт. + * + * При ошибке записи файл остаётся открытым — вызывающая сторона обязана + * вызвать 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 при успехе. + */ +static bool step_write(bool *p_file_left_open, 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); + } + + bsp_usb_cdc_poll(); + + FRESULT result = f_open(&g_s_file, USD_TEST_FILE, FA_WRITE | FA_CREATE_ALWAYS); + + if (result != FR_OK) + { + char detail[TEST_DETAIL_SIZE]; + (void) snprintf(detail, sizeof(detail), "open failed: %d", (int) result); + *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); + + bsp_usb_cdc_poll(); + + if (result != FR_OK || bytes_written != USD_TEST_SIZE) + { + char detail[TEST_DETAIL_SIZE]; + if (result != FR_OK) + { + (void) snprintf(detail, sizeof(detail), "write failed: %d", (int) result); + } + else + { + (void) snprintf(detail, sizeof(detail), "write incomplete: %u/%u", + (unsigned int) bytes_written, (unsigned int) USD_TEST_SIZE); + } + *p_out = make_fail(detail); + return false; + } + + result = f_close(&g_s_file); + *p_file_left_open = false; + bsp_usb_cdc_poll(); + + if (result != FR_OK) + { + char detail[TEST_DETAIL_SIZE]; + (void) snprintf(detail, sizeof(detail), "close after write failed: %d", (int) result); + *p_out = make_fail(detail); + return false; + } + + cli_send("{\"type\":\"progress\",\"test\":\"usd\"," + "\"step\":\"write\",\"status\":\"ok\"}\n"); + return true; +} + +/* ── Шаг 5: Read + Compare ──────────────────────────────────────────────── */ + +/** + * @brief Открыть FWTEST.TMP на чтение, прочитать USD_TEST_SIZE байт, + * сравнить с 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 при успехе. + */ +static bool step_read_compare(bool *p_file_left_open, 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); + + if (result != FR_OK) + { + char detail[TEST_DETAIL_SIZE]; + (void) snprintf(detail, sizeof(detail), "open for read failed: %d", (int) result); + *p_out = make_fail(detail); + return false; + } + + *p_file_left_open = true; + + (void) memset(g_s_read_buf, 0, USD_TEST_SIZE); + + UINT bytes_read = 0U; + result = f_read(&g_s_file, g_s_read_buf, USD_TEST_SIZE, &bytes_read); + bsp_usb_cdc_poll(); + + if (result != FR_OK || bytes_read != USD_TEST_SIZE) + { + char detail[TEST_DETAIL_SIZE]; + (void) snprintf(detail, sizeof(detail), "read failed: %d", (int) result); + *p_out = make_fail(detail); + return false; + } + + result = f_close(&g_s_file); + *p_file_left_open = false; + bsp_usb_cdc_poll(); + + if (result != FR_OK) + { + char detail[TEST_DETAIL_SIZE]; + (void) snprintf(detail, sizeof(detail), "close after read failed: %d", (int) result); + *p_out = make_fail(detail); + return false; + } + + for (uint32_t i = 0U; i < USD_TEST_SIZE; i++) + { + if (g_s_read_buf[i] != g_s_write_buf[i]) + { + char detail[TEST_DETAIL_SIZE]; + (void) snprintf(detail, sizeof(detail), "compare failed at offset %u", + (unsigned int) i); + *p_out = make_fail(detail); + return false; + } + } + + cli_send("{\"type\":\"progress\",\"test\":\"usd\"," + "\"step\":\"read_compare\",\"status\":\"ok\"}\n"); + return true; +} + +/* ── Реализация тест-модуля ─────────────────────────────────────────────── */ + +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; + + /* Шаг 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)) + { + usd_cleanup(file_open, true); + return fail_result; + } + + if (!step_read_compare(&file_open, &fail_result)) + { + usd_cleanup(file_open, true); + return fail_result; + } + + /* Успех: файл закрыт внутри step_read_compare, раздел ещё смонтирован */ + usd_cleanup(false, true); + + return (test_result_t){ + .status = TEST_STATUS_PASS, + .duration_ms = 0U, + .detail = { 0 }, + }; +} + +/* ── Дескриптор модуля ──────────────────────────────────────────────────── */ + +const test_module_t K_TEST_USD = { + .id = "usd", + .name = "microSD (SDIO)", + .critical = false, + .requires_hil = false, + .pre_confirm_prompt = "Вставьте карту microSD и нажмите OK", + .init = NULL, + .run = usd_run, + .deinit = NULL, +}; \ No newline at end of file