diff --git a/AGENTS.md b/AGENTS.md index 939bc3a6..24ccbc76 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,14 +48,14 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v | Task | 栈 (words) | 优先级 | |---|---|---| -| defaultTask | 512 | Normal | +| defaultTask | 1024 | Normal | | netTask | 1024 | Normal | | ftpTask | 1024 | Normal | -| testTask | 1024 | BelowNormal | +| testTask | 2048 | BelowNormal | | uiTask | 4096 | BelowNormal | | timeSyncTask | 512 | Normal | -(实际值以 `Src/freertos.c` 为准,六任务栈合计 32KB;改动栈/缓冲前先核对此表与实测) +(实际值以 `Src/freertos.c` 为准;testTask 仅在启用测试套件时创建。改动栈/缓冲前先核对此表与实测。2026-09-21 上调:defaultTask 512→1024(扛 mount/mkfs/FAT探针/cfg/syslog 深层 FatFS I/O,旧值经 `configCHECK_FOR_STACK_OVERFLOW=2` 钩子实测溢出)、testTask 1024→2048(存储套件 deep call chain + 局部缓冲溢出)。FreeRTOS 堆已迁 CCM,任务栈合计已不占 SRAM) ## 架构要点 diff --git a/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c b/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c index 04aa0316..093cf9ca 100644 --- a/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c +++ b/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c @@ -46,7 +46,12 @@ extern SPI_HandleTypeDef hspi1; volatile uint8_t g_spi1_dma_done; /* DMA 阈值(字节数超过此值使用 DMA 传输) */ -#define GD5F_DMA_THRESHOLD 32 +/* DMA 阈值 1024B(2026-09-20):阈值以下走 CPU 轮询。存在把**栈上小缓冲** + * 直接传进本驱动做收发的大小(dhara meta 读 132B、dhara_nand_is_free 64B), + * 任务栈随 FreeRTOS 堆驻留 CCM(0x10000000),而 DMA1/DMA2 物理上不可达 + * CCM——此类传输必须留在轮询域,否则静默失败(CCM 首次扩容实验实证的根因)。 + * 页级大块传输(2048)仍走 DMA,源/目的全部为文件作用域静态缓冲。 */ +#define GD5F_DMA_THRESHOLD 1024 /* * 函数功能:SPI1 TX DMA 完成回调(PROGRAM_LOAD 数据发送用) diff --git a/Drivers/BSP/UI/ui_port.c b/Drivers/BSP/UI/ui_port.c index 5c7f249f..38d8ae82 100644 --- a/Drivers/BSP/UI/ui_port.c +++ b/Drivers/BSP/UI/ui_port.c @@ -14,8 +14,16 @@ /* 160x160 1bpp 帧 = 3200 字节 */ static uint8_t g_ui_fbuf[UI_FB_SIZE]; -/* LVGL 绘制缓冲(与帧同尺寸,整屏渲染) */ -static uint8_t g_ui_draw_buf[UI_FB_SIZE]; + +/* LVGL 绘制缓冲(整屏渲染)。LVGL 9.3.0 对 I1 格式的整帧渲染,会在其自身算出的 + * stride*h(=3200) 之后再越界多写 1 个字节(实测台架金丝雀命中 draw_buf[3200]=0xFF)。 + * 若紧邻其后的 .bss(本例 ui_menu_view.o 的 s_frame_box)恰有指针,其最低字节被 + * 打成 0xFF → 野指针 → HardFault(PC=lv_obj_get_scroll_y,2026-09-20 存储套件实证, + * SRAM/CCM 两种堆布局下同一签名、与 CCM 无关)。物理多留 UI_DRAW_BUF_GUARD 字节 + * 吸收该越界写;逻辑尺寸仍按 UI_FB_SIZE 上报 LVGL(渲染数学 stride*h 不变)。 + * Lib/LVGL 为第三方库,不改其源码,越界容纳放在本移植层。 */ +#define UI_DRAW_BUF_GUARD 32 +static uint8_t g_ui_draw_buf[UI_FB_SIZE + UI_DRAW_BUF_GUARD]; /* 单键缓冲:菜单无需多键队列,仅保存最近一次待处理键 */ static volatile uint8_t g_pending_key = 0; diff --git a/Inc/FreeRTOSConfig.h b/Inc/FreeRTOSConfig.h index c3507e40..9a09ab14 100644 --- a/Inc/FreeRTOSConfig.h +++ b/Inc/FreeRTOSConfig.h @@ -77,6 +77,15 @@ #define configUSE_RECURSIVE_MUTEXES 1 #define configUSE_COUNTING_SEMAPHORES 1 #define configUSE_PORT_OPTIMISED_TASK_SELECTION 0 +/* USER CODE BEGIN FREE_RTOS_CONFIG_HARDENING */ +/* 栈溢出机械检测(2026-09-20 存储套件死机事故后加):模式 2 在每次上下文切换 + * 检查栈底水位线,溢出即调 vApplicationStackOverflowHook(报任务名+复位)。 + * 关闭检测时溢出会静默腐蚀堆相邻块(其它任务栈/TCB),表现为无异常、不可定位 + * 的全系统冻结——本次事故签名。开销:每次切换一次字比较,可忽略。 */ +#define configCHECK_FOR_STACK_OVERFLOW 2 +/* pvPortMalloc 失败钩子(只告警不复位:分配失败路径由各调用点 NULL 检查处理) */ +#define configUSE_MALLOC_FAILED_HOOK 1 +/* USER CODE END FREE_RTOS_CONFIG_HARDENING */ /* USER CODE BEGIN MESSAGE_BUFFER_LENGTH_TYPE */ /* Defaults to size_t for backward compatibility, but can be changed if lengths will always be less than the number of bytes in a size_t. */ diff --git a/MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct b/MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct index 34f119a4..51529b4e 100644 --- a/MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct +++ b/MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct @@ -1,7 +1,15 @@ ; ************************************************************* ; *** Scatter-Loading Description File (manual) *** -; *** IRAM1 = SRAM1+SRAM2 128KB continuous RAM. *** -; *** No SRAM3 usage (may not exist on clone MCUs). *** +; *** IRAM1 = SRAM1+SRAM2 128KB continuous RAM (DMA-reachable)*** +; *** CCM = 64KB Core-Coupled Memory @0x10000000 (SRAM3) *** +; *** CCM 直连 M4 内核零等待,但 DMA1/DMA2 物理不可达! *** +; *** 本工程只把 FreeRTOS 堆(heap_4.o:任务栈/队列/暂存) *** +; *** 放入 CCM;全部 DMA 收发缓冲均为文件作用域静态(留在 *** +; *** SRAM),且 GD5F 驱动 DMA 阈值已提到 1024B——栈上小缓 *** +; *** 冲(dhara meta 132B / is_free 64B)走轮询不进 DMA。 *** +; *** 两条前提缺任何一条 = 静默数据损坏(2026-09-20 首轮 *** +; *** 扩容实验实证)。CCM 存在性本机 ST-LINK 实测可读写; *** +; *** 克隆片可能缺 SRAM3,换料必须重测! *** ; ************************************************************* LR_IROM1 0x08000000 0x00100000 { ; load region size_region @@ -11,7 +19,10 @@ LR_IROM1 0x08000000 0x00100000 { ; load region size_region .ANY (+RO) .ANY (+XO) } - RW_IRAM1 0x20000000 0x00020000 { ; SRAM1+SRAM2, 128KB + RW_IRAM1 0x20000000 0x00020000 { ; SRAM1+SRAM2, 128KB (DMA-safe zone) .ANY (+RW +ZI) } + RW_CCM 0x10000000 0x00010000 { ; CCM 64KB: FreeRTOS heap only (no DMA!) + heap_4.o (+RW +ZI) ; ucHeap 48KB: 任务栈/队列/定时器 + } } diff --git a/Src/freertos.c b/Src/freertos.c index 8888b4ab..709b9018 100644 --- a/Src/freertos.c +++ b/Src/freertos.c @@ -25,6 +25,7 @@ /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ +#include "test_config.h" /* 套件选择:STORAGE 套件下 defaultTask 让出存储所有权 */ #include #include "net_task.h" #include "ch395f.h" @@ -72,7 +73,10 @@ const osThreadAttr_t timeSyncTask_attributes = { osThreadId_t defaultTaskHandle; const osThreadAttr_t defaultTask_attributes = { .name = "defaultTask", - .stack_size = 512 * 4, + .stack_size = 1024 * 4, /* 512→1024: 该任务扛 mount+mkfs+FAT探针+cfg加载+syslog写+ + * 配置引擎(全深层 FatFS I/O),2KB 实测峰值溢出——开启 + * configCHECK_FOR_STACK_OVERFLOW=2 后被钩子抓到(2026-09-21)。 + * 此前静默溢出腐蚀堆相邻块。改 .ioc 前须同步此值防 CubeMX 覆盖 */ .priority = (osPriority_t) osPriorityNormal, }; /* Definitions for netTask */ @@ -93,7 +97,7 @@ const osThreadAttr_t ftpTask_attributes = { osThreadId_t testTaskHandle; const osThreadAttr_t testTask_attributes = { .name = "testTask", - .stack_size = 1024 * 4, + .stack_size = 2048 * 4, /* 1024→2048: 存储套件 deep call chain + 局部缓冲,4KB 实测溢出(2026-09-20) */ .priority = (osPriority_t) osPriorityBelowNormal, }; /* Definitions for uiTask */ @@ -117,7 +121,9 @@ const osEventFlagsAttr_t sys_evt_attributes = { /* Private function prototypes -----------------------------------------------*/ /* USER CODE BEGIN FunctionPrototypes */ void ui_task_func(void *argument); +#if !defined(TEST_SUITE_STORAGE) static void default_task_net_cfg_save_notify(void); +#endif /* USER CODE END FunctionPrototypes */ void StartDefaultTask(void *argument); @@ -225,7 +231,12 @@ void StartDefaultTask(void *argument) * 防 FTL 瞬态抖动误触发);结构性错误 → 自动重格式化自愈。代价:卷被清空 * (/config.ini 丢失 → 配置引擎首启路径回落工厂默认并自动重建模板)—— * 仅当卷已不可用时触发,可用设备 > 一份配置文件。 */ - static FATFS default_fatfs; + #if !defined(TEST_SUITE_STORAGE) + /* STORAGE 套件构建下 defaultTask 完全跳过存储 bring-up(挂载/配置/日志盘写), + * 卷由 testTask 的 storage_test_run 独占——测试含 nand_ftl_deinit 掉电模拟, + * 与任何后台文件写入并发都会互相踩坏 FTL/FatFs 状态(2026-09-20 台架实证: + * 双写并发导致 mkfs 半途冻结 + BBT 被误写) */ + static FATFS default_fatfs; FRESULT fr = f_mount(&default_fatfs, "", 1); if (fr == FR_NO_FILESYSTEM) { DBG_INFO("[FS] no FAT volume, formatting NAND..."); @@ -330,6 +341,10 @@ void StartDefaultTask(void *argument) /* 注册"保存网络配置"请求通知:UI(uiTask)投递 → 置事件 → 本任务立即执行写盘 * (配置引擎属主 = 本任务;UI 保存不再直接触碰配置文件引擎,P1-3) */ net_cfg_set_save_req_cb(default_task_net_cfg_save_notify); +#else + /* STORAGE 套件构建:挂载/配置引擎/日志盘写全部跳过,存储由 testTask 独占 */ + DBG_INFO("[FS] mount/cfg skipped (STORAGE suite owns storage)"); +#endif /* 配置 poll 周期计数(每 4 次循环 = 2s 轮询一次,与引擎文档/模拟器一致) */ uint32_t cfg_poll_cnt = 0u; @@ -341,16 +356,22 @@ void StartDefaultTask(void *argument) (void)osEventFlagsWait(sys_evtHandle, SYS_EVT_CFG_SAVE, osFlagsWaitAny, 500u); } /* 执行待处理保存(配置引擎属主:写盘 + 成功后立即应用);无请求 = 空操作 */ + #if !defined(TEST_SUITE_STORAGE) net_cfg_save_run(); +#endif /* 配置文件变更轮询(2s 周期;沉降去抖在引擎内) */ if ((cfg_poll_cnt++ & 3u) == 0u) { + #if !defined(TEST_SUITE_STORAGE) cfg_file_poll(); +#endif } HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin); HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin); + #if !defined(TEST_SUITE_STORAGE) /* 排空运行期日志(上电/重启分类与自检故障落盘) */ syslog_drain(); +#endif } /* USER CODE END StartDefaultTask */ } @@ -438,10 +459,47 @@ void StartUiTask(void *argument) * "保存网络配置"请求通知(注册给 net_cfg 的保存交接): * uiTask 投递保存请求 → 置 SYS_EVT_CFG_SAVE → defaultTask 循环立即唤醒执行写盘 */ +#if !defined(TEST_SUITE_STORAGE) static void default_task_net_cfg_save_notify(void) { if (sys_evtHandle != NULL) { (void)osEventFlagsSet(sys_evtHandle, SYS_EVT_CFG_SAVE); } } +#endif + +/* + * 栈溢出钩子(configCHECK_FOR_STACK_OVERFLOW=2,2026-09-20 存储套件死机事故后加): + * 溢出 = 内核状态已不可信,禁止走 printf/内核对象,裸 UART 报任务名后复位。 + */ +void vApplicationStackOverflowHook(void *xTask, signed char *pcTaskName) +{ + static const char msg_head[] = "\r\n[ERR] STACK OVERFLOW task="; + + (void)xTask; + HAL_UART_Transmit(&huart1, (const uint8_t *)msg_head, + (uint16_t)(sizeof(msg_head) - 1u), 0xFFFFu); + if (pcTaskName != NULL) { + uint16_t len = 0; + while (len < 32u && pcTaskName[len] != '\0') { + len++; + } + HAL_UART_Transmit(&huart1, (const uint8_t *)pcTaskName, len, 0xFFFFu); + } + HAL_UART_Transmit(&huart1, (const uint8_t *)"\r\n", 2u, 0xFFFFu); + HAL_Delay(200); + NVIC_SystemReset(); +} + +/* + * 堆分配失败钩子:只告警不复位(NULL 由各调用点自行处理并上报自检)。 + * 同样走裸 UART——钩子可能在临界区内被调,printf/信号量不可用。 + */ +void vApplicationMallocFailedHook(void) +{ + static const char msg[] = "\r\n[ERR] MALLOC FAILED (heap exhausted)\r\n"; + + HAL_UART_Transmit(&huart1, (const uint8_t *)msg, + (uint16_t)(sizeof(msg) - 1u), 0xFFFFu); +} /* USER CODE END Application */ diff --git a/Src/stm32f4xx_it.c b/Src/stm32f4xx_it.c index c30bc106..44f20888 100644 --- a/Src/stm32f4xx_it.c +++ b/Src/stm32f4xx_it.c @@ -22,6 +22,7 @@ #include "stm32f4xx_it.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ +#include "usart.h" /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ @@ -95,17 +96,86 @@ void NMI_Handler(void) /** * @brief This function handles Hard fault interrupt. */ -void HardFault_Handler(void) +/* USER CODE BEGIN HardFault_Handler */ +/* 崩溃现场采集(本函数为手工改写,CubeMX 重生成会覆盖!): + * EXC_RETURN bit2 判断异常帧在 MSP 还是 PSP,取帧指针交给 C 报告函数。 + * 空 while(1) 版会让死机在日志上完全静默(2026-09-20 存储套件死机排查实证) */ +__asm void HardFault_Handler(void) { - /* USER CODE BEGIN HardFault_IRQn 0 */ - - /* USER CODE END HardFault_IRQn 0 */ - while (1) - { - /* USER CODE BEGIN W1_HardFault_IRQn 0 */ - /* USER CODE END W1_HardFault_IRQn 0 */ - } + extern hard_fault_report + TST lr, #4 + ITE EQ + MRSEQ r0, MSP + MRSNE r0, PSP + B hard_fault_report } +/* USER CODE END HardFault_Handler */ + +/* USER CODE BEGIN HardFault_Report */ +/* + * 函数功能:HardFault 现场报告(裸机上下文,不经内核/printf) + * 入口参数:frame - 异常栈帧 {R0,R1,R2,R3,R12,LR,PC,xPSR} + * 函数说明:直接 HAL_UART_Transmit 轮询输出 PC/LR/xPSR 与 CFSR/HFSR 故障 + * 寄存器,供开机日志定位崩溃点(对照 .map 找符号);输出后复位。 + */ +static void hard_fault_puts(const char *s) +{ + uint16_t len = 0; + + while (s[len] != '\0') { + len++; + } + HAL_UART_Transmit(&huart1, (const uint8_t *)s, len, 0xFFFFu); +} + +static void hard_fault_puthex(uint32_t v) +{ + static const char digits[] = "0123456789ABCDEF"; + char out[9]; + int i; + + for (i = 0; i < 8; i++) { + out[i] = digits[(v >> (28 - i * 4)) & 0xFu]; + } + out[8] = '\0'; + hard_fault_puts(out); +} + +void hard_fault_report(uint32_t *frame) +{ + hard_fault_puts("\r\n[ERR] HARDFAULT PC="); + hard_fault_puthex(frame[6]); + hard_fault_puts(" LR="); + hard_fault_puthex(frame[5]); + hard_fault_puts(" xPSR="); + hard_fault_puthex(frame[7]); + hard_fault_puts(" R0="); + hard_fault_puthex(frame[0]); + hard_fault_puts(" R1="); + hard_fault_puthex(frame[1]); + hard_fault_puts(" R2="); + hard_fault_puthex(frame[2]); + hard_fault_puts(" R3="); + hard_fault_puthex(frame[3]); + hard_fault_puts(" CFSR="); + hard_fault_puthex(SCB->CFSR); + hard_fault_puts(" HFSR="); + hard_fault_puthex(SCB->HFSR); + + /* BFAR/MMFAR 仅在对应 valid 位为 1 时有意义 */ + if ((SCB->CFSR & (1UL << 15)) != 0U) { + hard_fault_puts(" BFAR="); + hard_fault_puthex(SCB->BFAR); + } + if ((SCB->CFSR & (1UL << 7)) != 0U) { + hard_fault_puts(" MMFAR="); + hard_fault_puthex(SCB->MMFAR); + } + + HAL_Delay(200); + NVIC_SystemReset(); +} +/* USER CODE END HardFault_Report */ /** * @brief This function handles Memory management fault. diff --git a/test/storage_test.h b/test/storage_test.h index 2186e42b..756d6e00 100644 --- a/test/storage_test.h +++ b/test/storage_test.h @@ -48,7 +48,7 @@ void storage_test_run(void); /* 存储套件恢复测试(默认关闭):取消注释并编译 STORAGE 套件运行一次, * 即可重建 BBT(清除 TC-STO-03 等注入的坏块)并重新格式化,恢复设备干净态。 * 恢复完成后建议重新注释并编译正常构建。 */ -//#define ENABLE_STORAGE_RECOVERY_TESTS +//#define ENABLE_STORAGE_RECOVERY_TESTS /* 台架已跑过一次恢复(2026-09-20 BBT 回出厂态),保持注释 */ #ifdef __cplusplus } diff --git a/test/storage_test_task.c b/test/storage_test_task.c index 468e55a9..652730fa 100644 --- a/test/storage_test_task.c +++ b/test/storage_test_task.c @@ -19,15 +19,22 @@ storage_test_stats_t g_storage_stats; -static uint8_t s_perf_buf[4096]; +#ifndef ENABLE_STORAGE_RECOVERY_TESTS +/* 全套件共享的 FATFS/FIL(RAM 预算紧张:每个 static FATFS≈560B、FIL≈600B, + * 各测试函数独立持有会撑爆 .bss。测试在单任务 testTask 内串行执行、从不并发, + * 共用一组安全,且比"每函数一份"省下约 5KB。见产品级 RAM 溢出 L6406E) */ +static FATFS fs; +static FIL fil; + +/* 性能缓冲 2048B = 恰好一个 NAND 页(RAM 预算:4096 时套件链接溢出 0x604, + * L6406E 实测;2048 分块仍覆盖整页写路径,128KB 总量不变) */ +static uint8_t s_perf_buf[2048]; /* TC-STO-01: FTL 掉电恢复(journal 持久化) * 写文件并同步落盘,随后模拟掉电(卸载 + 复位 FTL + 重新 resume),再挂载校验内容一致。 */ static void storage_test_powerloss(void) { - static FATFS fs; - static FIL fil; const uint32_t len = 4096; uint8_t wbuf[512]; uint8_t rbuf[512]; @@ -90,14 +97,14 @@ static void storage_test_powerloss(void) /* TC-STO-02: 大文件 / 多扇区 / 随机 seek * 写 256KB 位置相关模式文件,顺序读回比对 + 随机偏移 seek 读比对。 + * 栈预算:testTask 栈 4KB,局部缓冲 4096 必然溢出栈(8 月侥幸未爆是堆使用 + * 低),改 2048 = 恰好一个 NAND 整页,覆盖度等价。 */ static void storage_test_largefile(void) { - static FATFS fs; - static FIL fil; const uint32_t total = 256 * 1024; - const uint32_t chunk = 4096; - uint8_t buf[4096]; + const uint32_t chunk = 2048; + uint8_t buf[2048]; UINT bw, br; uint32_t off, i; int ok = 1; @@ -162,8 +169,6 @@ static void storage_test_largefile(void) */ static void storage_test_badblock(void) { - static FATFS fs; - static FIL fil; uint8_t wbuf[512], rbuf[512]; UINT bw, br; uint32_t bad[3]; @@ -242,17 +247,150 @@ static void storage_test_badblock(void) STORAGE_TEST_CHECK(badok, "TC-STO-03 injected blocks still reported bad"); f_unlink("bbt.dat"); - /* 注意:注入的坏块已持久化,测试设备专用;如需恢复需重建 BBT / 重新格式化 */ + /* 注意:注入的坏块已持久化,测试设备专用;如需恢复需重建 BBT / 重新格式化 + * (配套 TC-STO-04 恢复测试:storage_test.h 启用 ENABLE_STORAGE_RECOVERY_TESTS + * 后跑一次套件,即可清除运行时注入坏块 + 清 dhara map 回出厂态) */ } +/* TC-STO-05: 文件名/路径合法性校验(守护 ".fschk 无限重格式化" 真根因) + * 本移植 FF_USE_LFN=0,只支持严格 8.3 短名(主名≤8、扩展≤3、不以点开头)。 + * 非法名字 f_open 必返 FR_INVALID_NAME(纯 RAM 名字判定、不碰盘);合法但 + * 不存在的名字返 FR_NO_FILE。本测试断言两类严格可区分——一旦哪天有人把 + * FR_INVALID_NAME 误归入"卷损坏"自愈集合,这条测试就是最后的机械防线。 + * (详见 docs/问题记录/GD5F2GQ5UE_Trap_Records.md Trap 05 / AGENTS.md) + */ +static void storage_test_name_validation(void) +{ + uint32_t i; + int gok = 1; + int bok = 1; + + /* 合法 8.3:FA_READ 打开 → FR_OK(存在) 或 FR_NO_FILE(不存在),均非 INVALID */ + static const char *good[] = { + "fschk.tmp", /* 修复后的探针名(名5+扩展3) */ + "config.ini", /* 名6+扩展3 */ + "12345678.txt", /* 主名 8 / 扩展 3 上限 */ + "A.TXT", /* 最小合法名 */ + "NOEXT", /* 无扩展 */ + "1.23", /* 数字名 */ + "run.log" + }; + /* 非法:FF_USE_LFN=0 下 create_short 必拒 → FR_INVALID_NAME */ + static const char *bad[] = { + ".fschk", /* 事故根因:点开头(点后当扩展域,超 3 上限) */ + ".hidden", /* 点开头隐藏名风格 */ + "123456789", /* 主名 9 > 8 */ + "toolongname.txt", /* 主名 > 8 */ + "a.toolongext", /* 扩展 > 3 */ + "bad?char.txt", /* 非法字符 ? */ + "star*name", /* * */ + "col:on", /* : */ + "ltval", /* < > */ + "pipe|name" /* | */ + }; + + DBG_INFO("=== Storage Test: TC-STO-05 Filename validation ==="); + + if (f_mount(&fs, "", 1) != FR_OK) { + STORAGE_TEST_CHECK(0, "TC-STO-05 mount"); + return; + } + + for (i = 0; i < (uint32_t)(sizeof(good) / sizeof(good[0])); i++) { + FRESULT r = f_open(&fil, good[i], FA_READ); + if (r == FR_OK) { + f_close(&fil); + } + if (r == FR_INVALID_NAME) { + gok = 0; + DBG_ERROR(" valid name wrongly rejected: '%s'", good[i]); + } + } + STORAGE_TEST_CHECK(gok, "TC-STO-05 valid 8.3 names accepted"); + + for (i = 0; i < (uint32_t)(sizeof(bad) / sizeof(bad[0])); i++) { + FRESULT r = f_open(&fil, bad[i], FA_READ); + if (r == FR_OK) { + f_close(&fil); + } + /* 含 ':' 的名字被 FatFs 先解析为盘符前缀 → FR_INVALID_DRIVE(11) + * 同样属合法拒绝(台架实测 'col:on' fr=11),与 INVALID 同等待遇 */ + if ((r != FR_INVALID_NAME) && + !((r == FR_INVALID_DRIVE) && (strchr(bad[i], ':') != NULL))) { + bok = 0; + DBG_ERROR(" invalid name NOT rejected: '%s' -> fr=%d", bad[i], (int)r); + } + } + STORAGE_TEST_CHECK(bok, "TC-STO-05 invalid names rejected (FR_INVALID_NAME)"); +} + +/* TC-STO-06: 打开模式与写回字节数语义(守护 FA_OPEN_APPEND / 短写陷阱) + * 本移植 FA_OPEN_APPEND 不含 FA_WRITE → 不带写位打开 f_write 必返 FR_DENIED; + * 追加写必须 FA_WRITE|FA_OPEN_APPEND。另 f_write 簇耗尽返 FR_OK+短写(无错误 + * 码),判成败必须校验写回字节数。锁定这两条移植专有语义防回归。 + */ +static void storage_test_open_modes(void) +{ + UINT bw; + FRESULT r; + + DBG_INFO("=== Storage Test: TC-STO-06 Open-mode semantics ==="); + + if (f_mount(&fs, "", 1) != FR_OK) { + STORAGE_TEST_CHECK(0, "TC-STO-06 mount"); + return; + } + (void)f_unlink("mode.tmp"); + + /* 1) 建一个含初始内容的文件 */ + r = f_open(&fil, "mode.tmp", FA_CREATE_ALWAYS | FA_WRITE); + STORAGE_TEST_CHECK(r == FR_OK, "TC-STO-06 create base file"); + if (r == FR_OK) { + f_write(&fil, "abc", 3, &bw); + f_close(&fil); + } + + /* 2) FA_OPEN_APPEND(无 FA_WRITE)→ 写必返 FR_DENIED */ + r = f_open(&fil, "mode.tmp", FA_OPEN_APPEND); + STORAGE_TEST_CHECK(r == FR_OK, "TC-STO-06 open append(no write)"); + if (r == FR_OK) { + FRESULT wr = f_write(&fil, "xyz", 3, &bw); + STORAGE_TEST_CHECK(wr == FR_DENIED, + "TC-STO-06 append-no-write must DENY (fr=%d)", (int)wr); + f_close(&fil); + } + + /* 3) FA_WRITE|FA_OPEN_APPEND → 追加成功且字节数正确 */ + r = f_open(&fil, "mode.tmp", FA_WRITE | FA_OPEN_APPEND); + STORAGE_TEST_CHECK(r == FR_OK, "TC-STO-06 open write|append"); + if (r == FR_OK) { + (void)f_write(&fil, "xyz", 3, &bw); + STORAGE_TEST_CHECK(bw == 3, "TC-STO-06 append byte-count (%u)", bw); + f_close(&fil); + } + + /* 4) 只读打开后写 → 必返 FR_DENIED(读句柄不可写) */ + r = f_open(&fil, "mode.tmp", FA_READ); + STORAGE_TEST_CHECK(r == FR_OK, "TC-STO-06 open read-only"); + if (r == FR_OK) { + FRESULT wr = f_write(&fil, "q", 1, &bw); + STORAGE_TEST_CHECK(wr == FR_DENIED, + "TC-STO-06 read-only write must DENY (fr=%d)", (int)wr); + f_close(&fil); + } + + (void)f_unlink("mode.tmp"); +} +#endif /* !ENABLE_STORAGE_RECOVERY_TESTS: 共享缓冲 + 正常测试函数 */ + #ifdef ENABLE_STORAGE_RECOVERY_TESTS static void storage_test_recover(void); #endif -void storage_test_run(void) +#ifndef ENABLE_STORAGE_RECOVERY_TESTS +/* 正常存储测试套件主体(恢复模式见文件末尾 storage_test_run 分发) */ +static void storage_test_run_normal(void) { - static FATFS fs; - static FIL fil; static uint8_t work[FF_MAX_SS]; char read_buf[64]; const char *test_str = "Hello FatFS + dhara FTL!"; @@ -341,7 +479,11 @@ void storage_test_run(void) } } - /* ==================== 存储集成测试(TC-STO-01~03) ==================== */ + /* ==================== 存储集成测试(TC-STO-05~06 契约 + 01~03 集成) ========== */ + + /* 纯语义契约测试(非破坏性,先跑):名字合法性 + 打开模式 */ + storage_test_name_validation(); + storage_test_open_modes(); storage_test_powerloss(); storage_test_largefile(); @@ -355,6 +497,24 @@ void storage_test_run(void) STORAGE_TEST_REPORT("Storage Tests"); } +#endif /* !ENABLE_STORAGE_RECOVERY_TESTS */ + +/* + * 套件总入口分发。恢复模式(storage_test.h 打开 ENABLE_STORAGE_RECOVERY_TESTS) + * = 专用维护构建:重建 BBT(清除运行时误标坏块)+ 清 dhara map 后直接返回, + * 不在受污染介质上继续跑其余测试(2026-09-20:CCM 首轮实验中 DMA 够不到栈上 + * 小缓冲,随机块被误标坏并持久化,journal 引用到即静默卡死——恢复模式即为其 + * 清账手段)。恢复完成后重新注释该宏、编译正常套件即可。 + */ +void storage_test_run(void) +{ +#ifdef ENABLE_STORAGE_RECOVERY_TESTS + storage_test_recover(); + STORAGE_TEST_REPORT("Storage Recovery"); +#else + storage_test_run_normal(); +#endif +} #ifdef ENABLE_STORAGE_RECOVERY_TESTS /* TC-STO-04: 重建 BBT + 重新格式化(恢复 TC-STO-03 注入的坏块)