CCM扩容FreeRTOS堆+DMA阈值1024防栈缓冲进CCM+HardFault现场报告与栈/内存钩子+修defaultTask栈溢出+LVGL绘制缓冲护栏+存储套件05/06与恢复模式

This commit is contained in:
2026-09-21 00:47:58 +08:00
parent 9bb5a8dcae
commit a7d4339e6f
9 changed files with 357 additions and 36 deletions
+3 -3
View File
@@ -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)
## 架构要点
+6 -1
View File
@@ -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 数据发送用)
+10 -2
View File
@@ -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;
+9
View File
@@ -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. */
+14 -3
View File
@@ -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: 任务栈/队列/定时器
}
}
+61 -3
View File
@@ -25,6 +25,7 @@
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "test_config.h" /* 套件选择:STORAGE 套件下 defaultTask 让出存储所有权 */
#include <string.h>
#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 */
+79 -9
View File
@@ -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.
+1 -1
View File
@@ -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
}
+174 -14
View File
@@ -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", /* : */
"lt<gt>val", /* < > */
"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 注入的坏块)