# test_usd: template

This commit is contained in:
Dmitry Akimov 2026-05-07 09:57:55 +03:00
parent 8a58b052b1
commit 10015af93a
5 changed files with 373 additions and 55 deletions

2
.gitignore vendored
View file

@ -74,4 +74,4 @@ __pycache__/
.claude
tools/host/.venv-host/
tools/host/.venv-host-win/
.zed/

View file

@ -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})

View file

@ -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();
}
}

View file

@ -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]))

View file

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