From 1d2a53bbc905b243b1b04a206940e2bd6762eff9 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Mon, 21 Sep 2026 18:38:23 +0800 Subject: [PATCH] =?UTF-8?q?E-31=E4=BF=AE=E5=A4=8D=EF=BC=9Adhara=20resume?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E6=8C=89=E9=94=99=E8=AF=AF=E5=88=86=E7=B1=BB?= =?UTF-8?q?=EF=BC=88=E4=BB=85=E7=9C=9F=E7=A9=BA=E7=99=BD=E5=88=A4=E6=96=B0?= =?UTF-8?q?=E5=8D=B7=EF=BC=89=EF=BC=8Cmount=E5=A4=B1=E8=B4=A5=E8=87=AA?= =?UTF-8?q?=E6=A3=80=E4=B8=8A=E6=8A=A5+=E4=B8=BB=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E6=9C=89=E7=95=8C=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Drivers/BSP/GD5F2GQ5UE/nand_ftl.c | 26 ++- Lib/dhara/journal.c | 69 ++++++-- Src/freertos.c | 281 ++++++++++++++++++------------ 3 files changed, 246 insertions(+), 130 deletions(-) diff --git a/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c b/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c index 49286333..a59d998a 100644 --- a/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c +++ b/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c @@ -6,6 +6,8 @@ * 创建日期:2026-07-20 * 修改记录: * 2026-07-20 王建锋 创建初始版本 + * 2026-09-21 E-31 resume 失败按错误分类:仅真空白判新卷,其余失败 + * 返回 mount 层(禁止静默清 map 触发整卷重格式化) */ /* 头文件包含区 */ @@ -218,10 +220,28 @@ DSTATUS disk_initialize(BYTE pdrv) { s_cache_dirty = 0; { - dhara_error_t err; + dhara_error_t err = DHARA_E_NONE; if (dhara_map_resume(&s_map, &err) < 0) { - DBG_INFO("No valid map found, creating fresh (err=%d)", err); - dhara_map_clear(&s_map); + if (err == DHARA_E_TOO_BAD) { + /* 扫描全程 I/O 干净且无 checkpoint = 真空白卷(出厂首启 / + * 显式格式化后);失败的 resume 已把 journal 复位为空态 → + * 按新卷处理,由上层自动建卷(此处 dhara_map_clear 为空操作: + * resume 失败时 count 已清零,map 本已为空)。 */ + DBG_INFO("No valid map found (err=%d), creating fresh", err); + dhara_map_clear(&s_map); + } else { + /* E-31(2026-09-21 审计 P0 修复):非"空白"错误(ECC / + * BAD_BLOCK / CORRUPT_MAP / 瞬态 I/O 等)= 真实完整性故障—— + * 芯片上 journal 可能仍持有有效 checkpoint。旧行为把任何失败 + * 当新卷清 map,上层随即整卷重格式化 → 单次瞬态毛刺即静默销毁 + * 全部用户数据。现把失败返回给 mount 层:f_mount 失败 → + * defaultTask 自检上报 "filesystem fault" 并保留卷;瞬态故障 + * 由主循环重试/下次上电重新 resume;重格式化只走显式命令 + * (nand_ftl_format)。 */ + DBG_ERROR("Map resume failed (err=%d), volume kept, " + "storage unavailable", err); + return STA_NOINIT; + } } else { DBG_INFO("Map resumed: %lu/%lu sectors used", dhara_map_size(&s_map), dhara_map_capacity(&s_map)); diff --git a/Lib/dhara/journal.c b/Lib/dhara/journal.c index 7bfe2677..d0214219 100644 --- a/Lib/dhara/journal.c +++ b/Lib/dhara/journal.c @@ -218,12 +218,20 @@ void dhara_journal_init(struct dhara_journal *j, /* Find the first checkpoint-containing block. If a block contains any * checkpoints at all, then it must contain one in the first checkpoint * location -- otherwise, we would have considered the block eraseable. + * + * Error contract (E-31 fix, 2026-09-21): DHARA_E_TOO_BAD is reported + * only when the scan completed without any I/O error (genuinely blank + * chip -- safe for the caller to start fresh). Any read failure in the + * scan is reported instead as the first such error, so callers can + * distinguish "no checkpoints exist" from "the scan was interrupted by + * I/O trouble" -- treating the latter as blank would wipe a live volume. */ static int find_checkblock(struct dhara_journal *j, dhara_block_t blk, dhara_block_t *where, dhara_error_t *err) { int i; + dhara_error_t io_err = DHARA_E_NONE; for (i = 0; (blk < j->nand->num_blocks) && (i < DHARA_MAX_RETRIES); i++) { @@ -231,19 +239,27 @@ static int find_checkblock(struct dhara_journal *j, (blk << j->nand->log2_ppb) | ((1 << j->log2_ppc) - 1); - if (!(dhara_nand_is_bad(j->nand, blk) || - dhara_nand_read(j->nand, p, - 0, 1 << j->nand->log2_page_size, - j->page_buf, err)) && - hdr_has_magic(j->page_buf)) { - *where = blk; - return 0; + if (dhara_nand_is_bad(j->nand, blk)) { + /* Bad block: no read performed, no I/O error */ + } else { + dhara_error_t read_err = DHARA_E_NONE; + + if (dhara_nand_read(j->nand, p, + 0, 1 << j->nand->log2_page_size, + j->page_buf, &read_err) < 0) { + if (io_err == DHARA_E_NONE) + io_err = read_err; + } else if (hdr_has_magic(j->page_buf)) { + *where = blk; + return 0; + } } blk++; } - dhara_set_error(err, DHARA_E_TOO_BAD); + dhara_set_error(err, + (io_err != DHARA_E_NONE) ? io_err : DHARA_E_TOO_BAD); return -1; } @@ -315,7 +331,11 @@ static dhara_block_t find_last_checkblock_any(struct dhara_journal *j, } /* Re-read the first-group checkpoint header of the given block into - * j->page_buf. Returns 0 on success. */ + * j->page_buf. Returns 0 on success. A failed read reports the I/O + * error; a successful read that no longer carries the checkpoint magic + * is an inconsistency (the block was verified to hold a checkpoint + * during the scan) and reports DHARA_E_CORRUPT_MAP -- never + * DHARA_E_TOO_BAD, which callers treat as a blank chip (E-31 fix). */ static int read_checkpage(struct dhara_journal *j, dhara_block_t blk, dhara_error_t *err) { @@ -323,10 +343,14 @@ static int read_checkpage(struct dhara_journal *j, dhara_block_t blk, ((1 << j->log2_ppc) - 1); if (dhara_nand_read(j->nand, p, 0, 1 << j->nand->log2_page_size, - j->page_buf, err) < 0 || - !hdr_has_magic(j->page_buf)) + j->page_buf, err) < 0) return -1; + if (!hdr_has_magic(j->page_buf)) { + dhara_set_error(err, DHARA_E_CORRUPT_MAP); + return -1; + } + return 0; } @@ -437,16 +461,20 @@ static int find_root(struct dhara_journal *j, dhara_page_t start, { const dhara_block_t blk = start >> j->nand->log2_ppb; int i = (start & ((1 << j->nand->log2_ppb) - 1)) >> j->log2_ppc; + dhara_error_t io_err = DHARA_E_NONE; while (i >= 0) { const dhara_page_t p = (blk << j->nand->log2_ppb) + ((i + 1) << j->log2_ppc) - 1; + dhara_error_t read_err = DHARA_E_NONE; - if (!dhara_nand_read(j->nand, p, + if (dhara_nand_read(j->nand, p, 0, 1 << j->nand->log2_page_size, - j->page_buf, err) && - (hdr_has_magic(j->page_buf)) && - (hdr_get_epoch(j->page_buf) == j->epoch)) { + j->page_buf, &read_err) < 0) { + if (io_err == DHARA_E_NONE) + io_err = read_err; + } else if ((hdr_has_magic(j->page_buf)) && + (hdr_get_epoch(j->page_buf) == j->epoch)) { j->root = p - 1; return 0; } @@ -454,7 +482,16 @@ static int find_root(struct dhara_journal *j, dhara_page_t start, i--; } - dhara_set_error(err, DHARA_E_TOO_BAD); + /* The checkpoint ring is already established by the time this + * runs (find_checkblock + epoch verification succeeded), so + * "no matching checkpoint" here means the stored checkpoint is + * inconsistent (read flakiness or real corruption): report an + * I/O error if one occurred, else DHARA_E_CORRUPT_MAP -- never + * DHARA_E_TOO_BAD, which callers treat as a blank chip + * (E-31 fix, 2026-09-21). + */ + dhara_set_error(err, + (io_err != DHARA_E_NONE) ? io_err : DHARA_E_CORRUPT_MAP); return -1; } diff --git a/Src/freertos.c b/Src/freertos.c index b64e8e9b..8ed84859 100644 --- a/Src/freertos.c +++ b/Src/freertos.c @@ -56,6 +56,10 @@ * 之后每周期只判定"是否出现更深峰值",无新峰值不打印(保持串口静默窗口) */ #define STACK_PROBE_FIRST_MS 5000u #define STACK_PROBE_PERIOD_MS 2000u +/* E-31 自愈策略(2026-09-21 审计 P0 修复):启动期存储挂载失败后, + * 主循环按 2s 周期重跑完整 bring-up 的次数上限;耗尽则维持无存储降级 + * (自检已上报、卷保留、不重格式化),等下次上电重新 resume */ +#define STORAGE_BRINGUP_RETRIES 3u /* USER CODE END PD */ /* Private macro -------------------------------------------------------------*/ @@ -128,6 +132,7 @@ void ui_task_func(void *argument); static void stack_probe_poll(void); /* 栈水位观测(可观测性,实现见文件末尾) */ #if !defined(TEST_SUITE_STORAGE) static void default_task_net_cfg_save_notify(void); +static FRESULT storage_bring_up(FATFS *fs); /* 存储 bring-up(实现见文件末尾) */ #endif /* USER CODE END FunctionPrototypes */ @@ -221,126 +226,36 @@ void StartDefaultTask(void *argument) * f_mount(forced=1) 会触发 disk_initialize() → nand_ftl_init() * (dhara_map_resume)。此前默认构建从未挂载,导致 FTP 的 * LIST/STOR/RETR 全部返回 550(f_opendir 失败)。 - * 若卷为空(FR_NO_FILESYSTEM,如恢复/坏块重建后清空),则首次自动 - * 格式化建卷(MKFS_PARM 与存储测试一致:FM_FAT32 / 无分区表)。 + * 完整 bring-up 序列(挂载 + 空卷自动建卷 + 健康探针 + 自愈重格式化 + * + 日志挂载 + 配置加载 + 就绪屏障)实现于 storage_bring_up()。 * - * 卷健康探针 + 自愈(2026-09-20,台架实测 /log 每次开机写失败 ×5 的 - * 根因处置):真根因 = s_append 打开模式缺 FA_WRITE 位——本 FatFs 移植 - * FA_OPEN_APPEND(0x30) = FA_OPEN_ALWAYS|FA_SEEKEND,不含 FA_WRITE(0x02), - * 未带写位打开的文件 f_write 恒返回 FR_DENIED(7) → 打开模式已改 - * FA_WRITE|FA_OPEN_APPEND(见 sys_log_mcu.c)。残余风险 = f_mount 仅校验 - * 首扇区 BPB,FAT 区陈旧/擦除(旧镜像时代遗留、换芯片、格式化中途掉电) - * 时挂载仍"成功",文件操作才报 FR_DENIED/FR_DISK_ERR/FR_INT_ERR 等结构 - * 性错误(注意本移植 f_write 簇分配失败返回 FR_OK+短写,探针须校验写回 - * 字节数,短写按 FR_DENIED 归一化)。挂载后做一次小写探针(两次确认, - * 防 FTL 瞬态抖动误触发);结构性错误 → 自动重格式化自愈。代价:卷被清空 - * (/config.ini 丢失 → 配置引擎首启路径回落工厂默认并自动重建模板)—— - * 仅当卷已不可用时触发,可用设备 > 一份配置文件。 */ + * E-31(2026-09-21 审计,P0 修复):dhara_map_resume 失败不再被当 + * "新卷"静默重格式化(扫描中任何一次瞬态毛刺都会销毁全部用户数据)—— + * nand_ftl 仅把 DHARA_E_TOO_BAD(真空白)判为新卷,其余失败使 + * disk_initialize 失败 → f_mount 失败 → 下方自检上报并保留卷; + * 瞬态失败由主循环有界重试(STORAGE_BRINGUP_RETRIES 次 × 2s), + * 成功后补跑"存储就绪"序列(日志挂载/配置加载/屏障置位,均幂等)。 + * 探针/重格式化自愈策略仍在 storage_bring_up 内(2026-09-20 修复, + * 小写两击确认、FR_INVALID_NAME 不入重格式化集、写保护类只上报, + * 详见该函数注释)。 */ + #if !defined(TEST_SUITE_STORAGE) + /* E-31 自愈:存储 bring-up 剩余重试次数(0 = 已就绪或已耗尽); + * 须在启动块之前声明(启动块失败路径写入、主循环读取) */ + uint32_t storage_retry = 0u; + #endif + #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..."); - static uint8_t fs_work[FF_MAX_SS]; - MKFS_PARM opts = { FM_FAT32, 0, 0, 0, 0 }; - /* 格式化前清空 FTL map 树:新卷从空树开始写,journal - * 不与旧卷残留共存(结构卫生)。注:这不是"无限重格式化"的根因—— - * 真根因是 FatFs 探针文件名非法(见下方探针注释)。 */ - (void)nand_ftl_format(); - fr = f_mkfs("", &opts, fs_work, sizeof(fs_work)); - if (fr == FR_OK) { - fr = f_mount(&default_fatfs, "", 1); - } else { - DBG_ERROR("[FS] f_mkfs failed (fr=%d)", (int)fr); - } - } - if (fr == FR_OK) { - /* 探针:/fschk.tmp 小写一次,验证 FAT 链真实可用。本移植 - * f_write 簇分配失败返回 FR_OK+短写 → 必须校验写回字节数;短写按 - * FR_DENIED 归一化(落入下方结构性集合触发重格式化)。 - * ⚠ 事故教训(2026-09-20 台架实证,"无限重格式化"真根因):本移植 - * FF_USE_LFN=0,ff.c create_short 把 ".fschk" 点后的 FSCHK 当扩展域 - * 解析(5 字符 > 3 上限)→ f_open 恒返 FR_INVALID_NAME(6)(纯 RAM - * 判定、不碰盘)→ 曾误入下方结构性错误集合 → 每 boot 假失败 → - * 无限重格式化。探针名必须严格 8.3:名≤8、扩展≤3、不以点开头。 */ -#define FS_VOL_PROBE(h) do { \ - FIL __chk; \ - static const uint8_t __chkbuf[8] = { 0 }; \ - UINT __bw = 0u; \ - FRESULT __wr; \ - (h) = f_open(&__chk, "/fschk.tmp", FA_WRITE | FA_OPEN_ALWAYS); \ - if ((h) == FR_OK) { \ - __wr = f_write(&__chk, __chkbuf, (UINT)sizeof(__chkbuf), &__bw); \ - (void)f_close(&__chk); \ - if (__wr == FR_OK && __bw == (UINT)sizeof(__chkbuf)) { \ - (h) = FR_OK; \ - (void)f_unlink("/fschk.tmp"); \ - } else { \ - (h) = (__wr != FR_OK) ? __wr : FR_DENIED; \ - } \ - } \ - } while (0) - FRESULT hfr; - FS_VOL_PROBE(hfr); - if (hfr != FR_OK) { - FS_VOL_PROBE(hfr); /* 两击确认 */ - } - if (hfr != FR_OK) { - /* ⚠ FR_INVALID_NAME 绝不可入本集合:它是路径名语法错误(纯 RAM 判定、 - * 不碰盘),与卷健康无关(曾误入 → "无限重格式化"事故) */ - if (hfr == FR_DENIED || hfr == FR_INVALID_OBJECT || - hfr == FR_INT_ERR || hfr == FR_DISK_ERR || - hfr == FR_NO_FILE || hfr == FR_NO_PATH || - hfr == FR_NO_FILESYSTEM) { - DBG_INFO("[FS] volume FAT invalid (fr=%d), reformatting...", - (int)hfr); - f_mount(NULL, "", 0); - /* 格式化前清空 FTL map 树(同首启路径:新卷从空树写起) */ - (void)nand_ftl_format(); - { - static uint8_t fs_work2[FF_MAX_SS]; - MKFS_PARM opts2 = { FM_FAT32, 0, 0, 0, 0 }; - fr = f_mkfs("", &opts2, fs_work2, sizeof(fs_work2)); - if (fr == FR_OK) { - fr = f_mount(&default_fatfs, "", 1); - } - } - if (fr == FR_OK) { - DBG_INFO("[FS] volume reformatted OK"); - } else { - DBG_ERROR("[FS] volume repair failed (fr=%d)", (int)fr); - syslog_post(SYSLOG_SELFTEST, "storage fault"); - } - } else { - /* 其余错误(写保护/卷锁/参数类)不重格式化——只上报, - * 防对写锁卷整卷擦除(FTL 磨损最大操作) */ - DBG_ERROR("[FS] volume probe failed (fr=%d)", (int)hfr); - syslog_post(SYSLOG_SELFTEST, "storage fault"); - fr = FR_INT_ERR; - } - } -#undef FS_VOL_PROBE - if (fr == FR_OK) { - DBG_INFO("[FS] FATFS mounted"); - /* 日志引擎挂载后处理:建 /log 目录 + 写"上电/重启"分类记录(唯一写者) */ - syslog_mcu_mounted(); - /* 加载网络配置进 RAM 副本(配置引擎属主 = 本任务;首启含旧档迁移; - * 文件缺失/损坏 = 工厂默认,不阻塞组网)。须在置位 STORAGE_READY 前, - * 使"存储就绪"屏障蕴含"配置已就绪",netTask 组网时配置恒有效 */ - (void)net_cfg_load(); - /* 广播"存储就绪"(= 卷挂载 + 配置加载完成),唤醒依赖文件系统的任务 */ - if (sys_evtHandle != NULL) { - osEventFlagsSet(sys_evtHandle, SYS_EVT_STORAGE_READY); - } - } - } else { - DBG_ERROR("[FS] f_mount failed (fr=%d)", (int)fr); + FRESULT fr = storage_bring_up(&default_fatfs); + if (fr != FR_OK) { /* 存储不可写:自检故障仅能滞留待发队列(串口已告警) */ syslog_post(SYSLOG_SELFTEST, "filesystem fault"); + /* E-31 自愈:启动主循环中的有界重试(每 2s 一次) */ + storage_retry = STORAGE_BRINGUP_RETRIES; } /* 注册"保存网络配置"请求通知:UI(uiTask)投递 → 置事件 → 本任务立即执行写盘 @@ -367,6 +282,23 @@ void StartDefaultTask(void *argument) /* 配置文件变更轮询(2s 周期;沉降去抖在引擎内) */ if ((cfg_poll_cnt++ & 3u) == 0u) { #if !defined(TEST_SUITE_STORAGE) + /* E-31 自愈:启动期挂载失败时,每 2s 重跑完整 bring-up—— + * 瞬态毛刺(如 dhara resume 扫描的瞬时 SPI 故障)通常第二次 + * 即恢复;成功后由 storage_bring_up 补跑启动时跳过的 + * "存储就绪"序列(日志挂载/配置加载/屏障置位,均幂等); + * 持续性故障耗尽重试则维持无存储降级(自检已在启动/耗尽时 + * 上报,卷保留、不重格式化,等下次上电重新 resume) */ + if (storage_retry > 0u) { + FRESULT rfr = storage_bring_up(&default_fatfs); + if (rfr == FR_OK) { + storage_retry = 0u; + DBG_INFO("[FS] storage bring-up OK on retry"); + } else if (--storage_retry == 0u) { + DBG_ERROR("[FS] storage retries exhausted (fr=%d), " + "storage disabled", (int)rfr); + syslog_post(SYSLOG_SELFTEST, "filesystem fault"); + } + } cfg_file_poll(); #endif } @@ -561,6 +493,133 @@ static void stack_probe_poll(void) (unsigned long)xPortGetMinimumEverFreeHeapSize()); } +/* + * 存储 bring-up:挂载 + 空卷自动建卷 + 卷健康探针(两击)+ 自愈重格式化 + * + 日志挂载 + 配置加载 + "存储就绪"屏障。 + * + * 由 defaultTask 调用:启动期一次;若失败,主循环每 2s 重试至多 + * STORAGE_BRINGUP_RETRIES 次(E-31 自愈策略,2026-09-21):dhara_map_resume + * 失败分类修复后(nand_ftl.c 仅真空白判新卷,其余失败保留卷并返回 + * disk_initialize 失败),重试让瞬态毛刺(SPI 瞬时故障/扫描窗口抖动) + * 在数秒内免重启恢复,成功时补跑启动期跳过的就绪序列(日志挂载/配置 + * 加载/屏障置位,三者均幂等、可重跑);持续故障耗尽重试后进入无存储 + * 降级态(自检已上报、卷保留、不重格式化,等下次上电重新 resume)。 + * + * 探针/重格式化策略(2026-09-20 台架实证修复,原启动块注释迁入): + * 小写两击确认防 FTL 瞬态抖动误触发;仅结构性错误集合(已排除 + * FR_INVALID_NAME)触发自愈重格式化;写保护/卷锁/参数类只上报不重格式化 + * (防对写锁卷整卷擦除)。 + */ +#if !defined(TEST_SUITE_STORAGE) +static FRESULT storage_bring_up(FATFS *fs) +{ + FRESULT fr = f_mount(fs, "", 1); + + if (fr == FR_NO_FILESYSTEM) { + /* 卷为空(如恢复/坏块重建后清空):自动格式化建卷 + * (MKFS_PARM 与存储测试一致:FM_FAT32 / 无分区表) */ + DBG_INFO("[FS] no FAT volume, formatting NAND..."); + static uint8_t fs_work[FF_MAX_SS]; + MKFS_PARM opts = { FM_FAT32, 0, 0, 0, 0 }; + /* 格式化前清空 FTL map 树:新卷从空树开始写,journal + * 不与旧卷残留共存(结构卫生)。注:这不是"无限重格式化"的根因—— + * 真根因是 FatFs 探针文件名非法(见下方探针注释)。 */ + (void)nand_ftl_format(); + fr = f_mkfs("", &opts, fs_work, sizeof(fs_work)); + if (fr == FR_OK) { + fr = f_mount(fs, "", 1); + } else { + DBG_ERROR("[FS] f_mkfs failed (fr=%d)", (int)fr); + } + } + + if (fr == FR_OK) { + /* 探针:/fschk.tmp 小写一次,验证 FAT 链真实可用。本移植 + * f_write 簇分配失败返回 FR_OK+短写 → 必须校验写回字节数;短写按 + * FR_DENIED 归一化(落入下方结构性集合触发重格式化)。 + * ⚠ 事故教训(2026-09-20 台架实证,"无限重格式化"真根因):本移植 + * FF_USE_LFN=0,ff.c create_short 把 ".fschk" 点后的 FSCHK 当扩展域 + * 解析(5 字符 > 3 上限)→ f_open 恒返 FR_INVALID_NAME(6)(纯 RAM + * 判定、不碰盘)→ 曾误入下方结构性错误集合 → 每 boot 假失败 → + * 无限重格式化。探针名必须严格 8.3:名≤8、扩展≤3、不以点开头。 */ +#define FS_VOL_PROBE(h) do { \ + FIL __chk; \ + static const uint8_t __chkbuf[8] = { 0 }; \ + UINT __bw = 0u; \ + FRESULT __wr; \ + (h) = f_open(&__chk, "/fschk.tmp", FA_WRITE | FA_OPEN_ALWAYS); \ + if ((h) == FR_OK) { \ + __wr = f_write(&__chk, __chkbuf, (UINT)sizeof(__chkbuf), &__bw); \ + (void)f_close(&__chk); \ + if (__wr == FR_OK && __bw == (UINT)sizeof(__chkbuf)) { \ + (h) = FR_OK; \ + (void)f_unlink("/fschk.tmp"); \ + } else { \ + (h) = (__wr != FR_OK) ? __wr : FR_DENIED; \ + } \ + } \ + } while (0) + FRESULT hfr; + FS_VOL_PROBE(hfr); + if (hfr != FR_OK) { + FS_VOL_PROBE(hfr); /* 两击确认 */ + } + if (hfr != FR_OK) { + /* ⚠ FR_INVALID_NAME 绝不可入本集合:它是路径名语法错误(纯 RAM 判定、 + * 不碰盘),与卷健康无关(曾误入 → "无限重格式化"事故) */ + if (hfr == FR_DENIED || hfr == FR_INVALID_OBJECT || + hfr == FR_INT_ERR || hfr == FR_DISK_ERR || + hfr == FR_NO_FILE || hfr == FR_NO_PATH || + hfr == FR_NO_FILESYSTEM) { + DBG_INFO("[FS] volume FAT invalid (fr=%d), reformatting...", + (int)hfr); + f_mount(NULL, "", 0); + /* 格式化前清空 FTL map 树(同首启路径:新卷从空树写起) */ + (void)nand_ftl_format(); + { + static uint8_t fs_work2[FF_MAX_SS]; + MKFS_PARM opts2 = { FM_FAT32, 0, 0, 0, 0 }; + fr = f_mkfs("", &opts2, fs_work2, sizeof(fs_work2)); + if (fr == FR_OK) { + fr = f_mount(fs, "", 1); + } + } + if (fr == FR_OK) { + DBG_INFO("[FS] volume reformatted OK"); + } else { + DBG_ERROR("[FS] volume repair failed (fr=%d)", (int)fr); + syslog_post(SYSLOG_SELFTEST, "storage fault"); + } + } else { + /* 其余错误(写保护/卷锁/参数类)不重格式化——只上报, + * 防对写锁卷整卷擦除(FTL 磨损最大操作) */ + DBG_ERROR("[FS] volume probe failed (fr=%d)", (int)hfr); + syslog_post(SYSLOG_SELFTEST, "storage fault"); + fr = FR_INT_ERR; + } + } +#undef FS_VOL_PROBE + if (fr == FR_OK) { + DBG_INFO("[FS] FATFS mounted"); + /* 日志引擎挂载后处理:建 /log 目录 + 写"上电/重启"分类记录(唯一写者) */ + syslog_mcu_mounted(); + /* 加载网络配置进 RAM 副本(配置引擎属主 = 本任务;首启含旧档迁移; + * 文件缺失/损坏 = 工厂默认,不阻塞组网)。须在置位 STORAGE_READY 前, + * 使"存储就绪"屏障蕴含"配置已就绪",netTask 组网时配置恒有效 */ + (void)net_cfg_load(); + /* 广播"存储就绪"(= 卷挂载 + 配置加载完成),唤醒依赖文件系统的任务 */ + if (sys_evtHandle != NULL) { + osEventFlagsSet(sys_evtHandle, SYS_EVT_STORAGE_READY); + } + } + } else { + DBG_ERROR("[FS] f_mount failed (fr=%d)", (int)fr); + } + + return fr; +} +#endif /* !TEST_SUITE_STORAGE */ + /* * "保存网络配置"请求通知(注册给 net_cfg 的保存交接): * uiTask 投递保存请求 → 置 SYS_EVT_CFG_SAVE → defaultTask 循环立即唤醒执行写盘