From 5538af829be59088a6bbf5ebbac168e42a52a20d Mon Sep 17 00:00:00 2001 From: super_agent_bot Date: Wed, 4 Nov 2026 18:16:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(nand-ftl):=20=E4=BC=A0=E8=BE=93=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E4=B8=8D=E5=86=8D=E8=AF=AF=E6=A0=87=E5=9D=8F=E5=9D=97?= =?UTF-8?q?=20+=20format=20=E4=B8=8D=E5=86=8D=E8=A2=AB=E8=84=8F=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E6=8C=A1=E6=AD=BB=EF=BC=88#79=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因(真机定案,问题 #79):旧 dhara_nand_erase/prog() 把一切失败一律 mark_bad,而 mark_block_bad 每次**立即持久化**板级 BBT(不可撤销)—— 一次传输层/等忙抖动就把好块永久记成坏块。台架 129 个好块(1016–1142 连续段)被固化,journal prepare_head() 连续 8 次撞 is_bad → TOO_BAD → Cache flush FAIL → reformat 又被同一页挡住 → 整卷禁用、重启不恢复。 - 修复①:只有芯片明确回 E_FAIL/P_FAIL(状态位证据)才标坏; 传输/超时失败只报 DHARA_E_BAD_BLOCK 让 dhara 重试,不标坏 - 修复②:nand_ftl_format() 尽力刷一次脏缓存,失败即丢弃并继续 dhara_map_clear()(格式化语义本就是销毁映射;旧实现第一步返回, 恢复动作在同一页死锁,f_mkfs 全在旧映射上必失败) - C4 还债:新增跨调用失败类快照 nand_ftl_fault_get/clear(20 B, resume/读/刷/同步四点位记账);defaultTask 探针按类分流, BSP_ERR_NO_SPACE 跳过重格式化(容量问题不该用 mkfs 解) - 哨兵与诊断:init 解锁块保护(A0h)后补回读校验(与 B0h ECC 同族 缺口,实测本板 A0h=0,非本次根因,留作哨兵);gd5f_bbt_load() 合并后坏块数 > 出厂扫描时打「BBT after merge」,现场可区分 真坏与误标固化 - 测试:主机单测 149→178 全绿;新增 format 死锁(TC-NANDPORT-800.. 813b)、传输失败不标坏(207 语义翻转 + 208/505);牙齿检查过—— 把修复回退即见红 - 真机:恢复动作 A 重建 BBT 清除已固化误标(3/3 PASSED,BBT version=71);产品镜像复烧 [FS] FATFS mounted、配置出厂模板 落盘、0 条 flush FAIL——#79 真机闭环(旧卷内容随 map clear 作废,台架卷为可弃数据) - 文档:存储架构说明 §10.4 口径修正 + 新增 §10.4.1/§10.4.2(页级 失败升级链与误标持久化机理),存储维护与容量恢复 §2 补板级 BBT 独立持久化注记 + §2.1(动作 A 此前静默失效),登记簿 C4 销项 - Keil 0 错 0 警(Code=255552,与烧录验证镜像一致);doc-links 213 文档通过 --- App/task/default_task.c | 63 +++-- Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c | 52 +++++ Drivers/BSP/GD5F2GQ5UE/nand_ftl.c | 174 +++++++++++++- Drivers/BSP/GD5F2GQ5UE/nand_ftl.h | 61 +++++ docs/任务规划/登记簿.md | 2 +- docs/架构设计/STM32F4-Base存储架构说明.md | 83 ++++++- docs/测试说明/专项验证/存储维护与容量恢复.md | 33 +++ scripts/ram_baseline.tsv | 2 +- scripts/size_attestation.json | 22 +- test/board/storage_test.h | 2 +- test/host/nand_port/nand_port_test.c | 228 ++++++++++++++++++- 11 files changed, 665 insertions(+), 57 deletions(-) diff --git a/App/task/default_task.c b/App/task/default_task.c index 642c3f8e..5db96784 100644 --- a/App/task/default_task.c +++ b/App/task/default_task.c @@ -48,6 +48,7 @@ #include "main.h" #include "ff.h" #include "nand_ftl.h" +#include "bsp_err.h" /* BSP_ERR_NO_SPACE / BSP_ERR_CRC:卷健康探针按失败类分流(C4) */ #include "net_cfg.h" #include "cfg_file.h" #include "run_par.h" /* 运行参数段落盘(「数据可信」这一项,配置引擎属主 = 本任务) */ @@ -523,30 +524,56 @@ static FRESULT storage_bring_up(FATFS *fs) { FS_VOL_PROBE(hfr); /* 两击确认 */ } if (hfr != FR_OK) { + /* C4 消费点(2026-11-04,问题 #79):探针只拿到 FRESULT,失败类在 + * disk_read/disk_write 返回时就被 FatFs 契约抹掉了。这里把 FTL 留下的 + * 跨调用快照读出来,**先判"重格式化有没有意义"再决定动作**—— + * 这正是 存储架构说明 §10.4 那条一直只写在文档里、没有消费者的判据。 + * + * 为什么必须先判:BSP_ERR_NO_SPACE 是 journal/映射层块池见底(介质健康), + * 重格式化只会把问题推回去(文档原文);而 BSP_ERR_CRC 才是"页级太坏, + * 换页/reformat 可能救"。两类都走 reformat 的话,块池见底会被反复擦盘。 */ + nand_ftl_fault_t fault; + + nand_ftl_fault_get(&fault); + if (fault.active != 0u) { + DBG_ERROR("[FS] probe failed (fr=%d), FTL last fault: op=%u class=%ld lpn=%lu count=%lu", + (int)hfr, (unsigned)fault.op, (long)fault.cls, + (unsigned long)fault.lpn, (unsigned long)fault.count); + } + /* ⚠ 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"); + /* 按失败类分流:块池见底 ≠ 介质坏。前者擦卷救不回来,只上报 + * (后续应走容量恢复,见 存储维护与容量恢复.md),后者才 reformat。 */ + if ((fault.active != 0u) && (fault.cls == (int32_t)BSP_ERR_NO_SPACE)) { + DBG_ERROR("[FS] volume probe failed: FTL out of space, " + "reformat skipped (needs capacity recovery, not mkfs)"); + syslog_post(SYSLOG_SELFTEST, "storage full"); + fr = FR_INT_ERR; } else { - DBG_ERROR("[FS] volume repair failed (fr=%d)", (int)fr); - syslog_post(SYSLOG_SELFTEST, "storage fault"); + 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 { /* 其余错误(写保护/卷锁/参数类)不重格式化——只上报, @@ -685,7 +712,7 @@ static void wave_store_drain(void) { } } if ((why == NULL) && (s_wscan_done == 0u)) { - FRESULT rr; + FRESULT rr = FR_OK; fr = f_opendir(&s_wdir, WAVE_ST_DIR); if (fr == FR_OK) { diff --git a/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c b/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c index cdfdc00a..4c31c088 100644 --- a/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c +++ b/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c @@ -8,6 +8,11 @@ * 2026-07-16 王建锋 创建初始版本 * 2026-07-17 王建锋 切换硬件 SPI,修正擦除地址,参考 NuttX 驱动 * 2026-08-27 补充函数注释(代码规范 V1.0);修正工厂坏块扫描为首页;增加 BBT 持久化 + * 2026-11-04 问题 #79 取证配套:① init 解锁块保护(A0h)后补回读校验—— + * 与 2026-10-23 修过的 ECC(B0h)同族的"只写不回读"缺口 + * (实测本板 A0h 已为 0,写保护非本次根因,校验留作哨兵); + * ② `gd5f_bbt_load()` 合并后若坏块数 > 出厂扫描数则打一条 + * "BBT after merge",让现场一眼区分真坏与误标固化。 */ /* 头文件包含区 */ @@ -839,6 +844,39 @@ int32_t gd5f2gq5ue_init(void) { DBG_ERROR("Unlock block protection failed: %d", ret); return ret; } + /* 回读 A0h 校验保护位**真正清零**(2026-11-04,问题 #79 取证发现)。 + * + * 为什么必须校验:本项与 2026-10-23 修过的 ECC(B0h) 是同一条缺陷的另一半—— + * `gd5f_set_feature()` 只写不回读,"写了但没生效"对上层完全不可见。 + * A0h 没清干净的后果比 ECC 没开更直接:**整片只读**。 + * + * 台架现场(11-04 17:40 本次启动,`repair-nand-volume` 取证): + * BBT scan: 0 bad blocks found ← 介质出厂坏块为 0 + * 读操作全部正常(Map resumed / FAT 探针能读到内容) + * 写操作**全部**失败:LPN=211(块 3)与 LPN=15(块 0)两个不同块 + * 连 nand_ftl_format() 后的 map sync 都失败 → storage disabled + * "读正常 + 写全废 + 跨多个无关块 + 出厂坏块为 0" 这组特征排除了 + * "某一页/某一块真坏"(那不会跨块、也不会让全新 map sync 失败), + * 指向芯片处于**写保护态**。 + * + * 判据:A0h bit0-3 (BP0..BP3) 与非易失的 BP 位组合决定保护范围; + * 读到非 0 即保护未解除,必须**明确失败**而不是继续跑(继续跑会让 + * 上层把"写保护"误判成"介质损坏",进而反复格式化——正是本单现场)。 + */ + { + uint8_t prot = 0; + + ret = gd5f_private_get_feature(GD5F_REG_PROTECT, &prot); + if (ret != GD5F_OK) { + DBG_ERROR("Read A0h failed: %d", ret); + return ret; + } + if (prot != 0u) { + DBG_ERROR("Block protection NOT cleared (A0h=0x%02X), NAND is read-only", + prot); + return GD5F_ERROR; + } + } DBG_INFO("Locating BBT pool..."); ret = gd5f_private_bbt_locate_pool(); @@ -854,6 +892,20 @@ int32_t gd5f2gq5ue_init(void) { DBG_ERROR("Load persisted BBT failed: %d (use scanned BBT)", ret); } + /* 【#79】持久化 BBT 若把连续段判坏(出厂扫描 0 坏块、底层写全部成功), + * 那是运行期误标被固化后的结果,不是介质坏。此处只打一条汇总, + * 让下次现场一眼能区分"真坏"与"误标"。 */ + { + uint32_t bad = 0; + for (uint32_t b = 0; b < GD5F_TOTAL_BLOCKS; b++) { + if ((s_bbt[b >> 3] >> (b & 7)) & 1) bad++; + } + if (bad > 0) { + DBG_ERROR("BBT after merge: %lu bad blocks (factory scan was 0)", + (unsigned long)bad); + } + } + DBG_INFO("NAND init OK"); return GD5F_OK; } diff --git a/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c b/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c index c9f7345d..82a78b1a 100644 --- a/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c +++ b/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c @@ -25,10 +25,31 @@ * 单一真源 + 两条负数组编译期守卫:越界上界依赖 * log2_ppb,换料时只改 GD5F_* 而忘改字面量会**静默** * 把守卫算大 = 越界守卫失效,现改为直接编译错 + * 2026-11-04 王建锋 问题 #79(台架 LPN=211 恒 TOO_BAD 致 f_mount 失败、 + * 整卷禁用)两处修复: + * ① `nand_ftl_format()` 不再被脏缓存 flush 失败挡住—— + * 旧实现先 flush、失败即 return,导致**恢复动作自己在 + * 同一页上死锁**(map 从未清过,f_mkfs 在旧映射上必失败)。 + * 格式化语义本就是销毁映射,故尽力刷一次后无条件丢缓存; + * ② 补 C4 跨调用失败类快照(`nand_ftl_fault_get/clear`), + * 让 disk_initialize/读/刷/同步的失败归类能穿过 FatFs 的 + * RES_ERROR 边界,供 defaultTask 探针按类分流(见 存储架构 + * 说明 §10.4);③ .c 自包含 include 本模块头(主机侧单测 + * 直接 #include 本文件,缺头则类型不可见)。 + * ④(真机取证后定案)`dhara_nand_erase/prog()` 不再把 + * **传输层/等忙失败**当坏块证据:只有芯片明确回 E_FAIL / + * P_FAIL 才标坏。真凶即在此——非坏类失败被标坏后由 + * mark_block_bad 立即持久化(不可撤销),台架 BBT 遂固化 + * 129 个误标块(1016–1142 连续段),journal prepare_head() + * 连续 8 次 is_bad → TOO_BAD → 整卷禁用;已固化的误标由 + * 恢复动作 A 重建 BBT 清除(见 存储维护与容量恢复 §2.1)。 */ /* 头文件包含区 */ #include +#include "nand_ftl.h" /* 本模块公开契约:失败快照类型(C4)与 nand_ftl_* 原型。 + * 必须自包含——旧版靠调用方先 include 本头,主机侧单测 + * 直接 #include "nand_ftl.c" 时类型不可见(实测编译错)。 */ #include "gd5f2gq5ue.h" #include "ff.h" #include "diskio.h" @@ -125,10 +146,19 @@ static uint8_t s_copy_buf[FTL_PAGE_SIZE]; /* 初始化状态 */ static uint8_t s_initialized; +/* 失败快照(C4):把"最近一次存储失败"的归类带出函数边界。 + * 为什么需要:disk_read/disk_write 对 FatFs 只能回 RES_ERROR(FatFs 契约), + * 失败类在 return 那一刻就被抹掉,上层因此无法区分「页级 TOO_BAD,重格式化可解」 + * 与「块池见底 / 映射损坏,重格式化只会把问题推回去」。 + * 多写者纪律:defaultTask 与 testTask 都会碰存储,故语义是"最近一次"而非账本; + * 并发下可能被另一路覆盖——诊断用途可接受,记在此处以免日后误当权威计数。 */ +static nand_ftl_fault_t s_fault; + /* ======================== 私有函数声明 ======================== */ static bsp_err_t ftl_flush_cache(void); static bsp_err_t ftl_read_page(dhara_sector_t lpn); +static void ftl_fault_note(uint32_t op, bsp_err_t cls, dhara_sector_t lpn); /* SPI 原语从 gd5f2gq5ue.h 获取 */ @@ -184,6 +214,52 @@ static bsp_err_t ftl_class_from_dhara(dhara_error_t err) { } } +/* ======================== 失败快照(C4) ======================== */ + +/* + * 函数功能:记录一次存储失败到跨调用快照 + * 入口参数:op - 失败的操作(NAND_FTL_OP_*) + * cls - 失败类(ftl_class_from_dhara 的结果) + * lpn - 涉及逻辑页号(未知传 NAND_FTL_LPN_NONE) + * 返回值:无 void + * 限定条件:可在任何上下文调用;本函数只写几个字,不加锁(见下方顺序说明) + * 函数说明:**不覆盖已有的非 NONE 归类除非新失败更具体**——不,刻意选择 + * "后写覆盖":快照回答的是"最近一次失败是什么",最靠近处置点的现场才有用。 + * count 单独累计,用来回答另一个问题:"是不是一直在撞同一页"(本单现场正是 + * LPN=211 反复失败 15 次)。两者分开存,避免为了统计把最近现场挤掉。 + * + * 写入顺序是有意的:先写内容字段,最后置 active=1(写侧无锁,读侧可能 + * 在任意指令边界被抢占)。这样读到 active=1 时内容字段必然已经写好; + * 最坏情况是读到旧内容 —— 诊断用途可接受,且不会比"没有快照"更差。 + */ +static void ftl_fault_note(uint32_t op, bsp_err_t cls, dhara_sector_t lpn) { + s_fault.op = op; + s_fault.cls = (int32_t)cls; + s_fault.lpn = (uint32_t)lpn; + if (s_fault.count < 0xFFFFFFFFu) { + s_fault.count++; + } + s_fault.active = 1u; +} + +void nand_ftl_fault_get(nand_ftl_fault_t *p_out) { + if (p_out == NULL) { + return; + } + /* 整结构拷贝:调用方拿到的是一致快照,不会一半新一半旧 */ + *p_out = s_fault; +} + +void nand_ftl_fault_clear(void) { + /* 先清 active 再清内容:反序会让读侧看到 active=0 但 count 仍是旧值, + * 虽然无害,但"active=0 时其余字段无意义"这条契约更该由写法保证 */ + s_fault.active = 0u; + s_fault.op = NAND_FTL_OP_NONE; + s_fault.cls = (int32_t)BSP_OK; + s_fault.lpn = NAND_FTL_LPN_NONE; + s_fault.count = 0u; +} + /* ======================== 页面缓存管理 ======================== */ static bsp_err_t ftl_flush_cache(void) { @@ -196,6 +272,7 @@ static bsp_err_t ftl_flush_cache(void) { bsp_err_t cls = ftl_class_from_dhara(err); /* 只在失败分支需要(cppcheck variableScope) */ DBG_ERROR("Cache flush FAIL: LPN=%lu dhara=%s class=%d", s_cached_lpn, ftl_err_name(err), cls); + ftl_fault_note(NAND_FTL_OP_FLUSH, cls, s_cached_lpn); return cls; } @@ -210,6 +287,7 @@ static bsp_err_t ftl_read_page(dhara_sector_t lpn) { bsp_err_t cls = ftl_class_from_dhara(err); /* 只在失败分支需要(cppcheck variableScope) */ DBG_ERROR("Page read FAIL: LPN=%lu dhara=%s class=%d", lpn, ftl_err_name(err), cls); + ftl_fault_note(NAND_FTL_OP_READ, cls, lpn); return cls; } @@ -243,9 +321,24 @@ int dhara_nand_erase(const struct dhara_nand *n, dhara_block_t b, dhara_error_t } ret = gd5f_block_erase(ftl_phy_block(b)); - if (ret == GD5F_ERASE_FAIL || ret != GD5F_OK) { - dhara_nand_mark_bad(n, b); - dhara_set_error(err, DHARA_E_BAD_BLOCK); + if (ret != GD5F_OK) { + if (ret == GD5F_ERASE_FAIL) { + /* 芯片明确回 E_FAIL:该块确实擦不净,标坏是正确处置 */ + DBG_ERROR("Erase failed with E_FAIL: block=%lu", (unsigned long)b); + dhara_nand_mark_bad(n, b); + dhara_set_error(err, DHARA_E_BAD_BLOCK); + } else { + /* 【#79 核心修复】传输层/等忙失败(GD5F_ERROR / GD5F_WAIT_EXPIRED) + * 不是坏块证据:芯片根本没回 E_FAIL,把块标坏等于把"线抖了一下" + * 固化成"这块永久报废",并立即写进持久化 BBT(不可撤销)。 + * 这类失败只报 BAD_BLOCK 让 dhara 重试,**不标坏**。 + * 真机取证(2026-11-04 17:53):出厂扫描 0 坏块、底层 prog/erase + * 全部成功,而持久化 BBT 把 1016–1142 连续段判坏,prepare_head() + * 连续 8 次撞 is_bad → DHARA_E_TOO_BAD → 整卷禁用。 */ + DBG_ERROR("Erase transport/timeout failure (not a bad block): " + "block=%lu ret=%d", (unsigned long)b, ret); + dhara_set_error(err, DHARA_E_BAD_BLOCK); + } return -1; } return 0; @@ -264,15 +357,27 @@ int dhara_nand_prog(const struct dhara_nand *n, dhara_page_t p, const uint8_t *d gd5f_write_enable(); ret = gd5f_program_load(0, data, FTL_PAGE_SIZE); if (ret != GD5F_OK) { - dhara_nand_mark_bad(n, p >> n->log2_ppb); + /* 【#79】装载失败是传输层问题(SPI/DMA/等忙),芯片未回 P_FAIL, + * 不得标坏块;只报 BAD_BLOCK 让 dhara 重试。 */ + DBG_ERROR("Program load transport failure (not a bad block): " + "page=%lu ret=%d", (unsigned long)phy_pg, ret); dhara_set_error(err, DHARA_E_BAD_BLOCK); return -1; } ret = gd5f_program_exec(phy_pg); - if (ret == GD5F_PROGRAM_FAIL || ret != GD5F_OK) { - dhara_nand_mark_bad(n, p >> n->log2_ppb); - dhara_set_error(err, DHARA_E_BAD_BLOCK); + if (ret != GD5F_OK) { + if (ret == GD5F_PROGRAM_FAIL) { + /* 芯片明确回 P_FAIL:页确实写不进去,标坏是正确处置 */ + DBG_ERROR("Program failed with P_FAIL: page=%lu", (unsigned long)phy_pg); + dhara_nand_mark_bad(n, p >> n->log2_ppb); + dhara_set_error(err, DHARA_E_BAD_BLOCK); + } else { + /* 【#79】传输层/等忙失败不是坏块证据,不标坏,只重试 */ + DBG_ERROR("Program transport/timeout failure (not a bad block): " + "page=%lu ret=%d", (unsigned long)phy_pg, ret); + dhara_set_error(err, DHARA_E_BAD_BLOCK); + } return -1; } @@ -431,6 +536,14 @@ DSTATUS disk_initialize(BYTE pdrv) { s_cached_lpn = (dhara_sector_t)-1; /* -1 表示缓存未加载;LPN 0 合法,不能用 0 作哨兵 */ s_cache_dirty = 0; + /* 失败快照初值:一次上电从"无失败"起算。刻意放在 map_init 之后、 + * resume 之前——resume 失败本身就是要被记进快照的第一件事。 */ + s_fault.active = 0u; + s_fault.op = NAND_FTL_OP_NONE; + s_fault.cls = (int32_t)BSP_OK; + s_fault.lpn = NAND_FTL_LPN_NONE; + s_fault.count = 0u; + { dhara_error_t err = DHARA_E_NONE; if (dhara_map_resume(&s_map, &err) < 0) { @@ -453,6 +566,9 @@ DSTATUS disk_initialize(BYTE pdrv) { DBG_ERROR("Map resume failed (dhara=%s class=%d), volume kept, " "storage unavailable", ftl_err_name(err), ftl_class_from_dhara(err)); + ftl_fault_note(NAND_FTL_OP_MAP_RESUME, + ftl_class_from_dhara(err), + (dhara_sector_t)NAND_FTL_LPN_NONE); return STA_NOINIT; } } else { @@ -598,6 +714,8 @@ DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { dhara_error_t err; if (dhara_map_sync(&s_map, &err) < 0) { DBG_ERROR("Map sync failed: %d", err); + ftl_fault_note(NAND_FTL_OP_SYNC, ftl_class_from_dhara(err), + (dhara_sector_t)NAND_FTL_LPN_NONE); return RES_ERROR; } } @@ -634,20 +752,52 @@ int nand_ftl_format(void) { } } + /* 处置开始:清掉历史失败快照,此后新失败重新记账 + * (否则一次旧失败会永久污染"本次格式化到底成没成"的判定) */ + nand_ftl_fault_clear(); + + /* 【本单核心修复 2026-11-04,问题 #79】 + * 旧实现先 `if (ftl_flush_cache() != BSP_OK) return GD5F_ERROR;`——而 + * ftl_flush_cache() 在失败分支**不清 s_cache_dirty**(那是刻意的:写不进去 + * 就不能假装写成功了),于是只要脏页落在"写不动的那个 LPN"上, + * 本函数就在第一道闸门返回,**dhara_map_clear() 永远执行不到**。 + * + * 台架现场(11-04 08:16 与 08:42 两次独立捕获,head/tail/epoch 一字不变): + * 探针写 /fschk.tmp → disk_write → flush LPN=211 失败(TOO_BAD) + * → 重试同页再次失败 → 判卷损坏 → 走本函数 → 又被同一页挡住 + * → f_mkfs 在旧映射上跑 → 失败 → 3 轮重试耗尽 → storage disabled。 + * 每次启动必现、断电重启不恢复,因为 s_cache_dirty 与 LPN=211 的脏数据 + * 在**同一次启动内**恒存,而每轮启动都会重新踩上同一条路。 + * + * 为什么"跳过 flush 直接丢缓存"在这里是正确的: + * 本函数的语义是**销毁整卷逻辑映射**(头文件:「破坏性:整卷逻辑地址→ + * 物理映射作废」,只允许显式命令/恢复流程调用;现存两个调用点 + * ——首启无卷与 FAT 探针判卷损坏——都紧接着 f_mkfs 重建卷)。既然这份 + * 脏页映射注定被扔掉,为它挡住格式化既救不回数据,又让**恢复动作本身 + * 永久失效**。故:尽力刷一次(成功则顺带落盘、日志更干净),失败就 + * 丢缓存并继续——这正是"格式化"该有的优先级。 + * + * ⚠ 不得把本改动挪到普通写路径:disk_write/disk_read 里的 flush 失败 + * 必须继续向上报错(那是"这次写没落地"的真相),只有本函数有权丢弃缓存。 + */ if (ftl_flush_cache() != BSP_OK) { - return GD5F_ERROR; + DBG_ERROR("format: dirty cache at LPN=%lu undiscardable, " + "dropping it (map is being destroyed anyway)", s_cached_lpn); } + /* 无条件失效页面缓存:格式化后旧映射作废,绝不能让它再被当成有效缓存 */ + s_cache_dirty = 0; + s_cached_lpn = (dhara_sector_t)-1; dhara_map_clear(&s_map); if (dhara_map_sync(&s_map, &err) < 0) { - DBG_ERROR("Map sync after clear failed: %d", err); + DBG_ERROR("Map sync after clear failed: %s class=%d", + ftl_err_name(err), ftl_class_from_dhara(err)); + ftl_fault_note(NAND_FTL_OP_SYNC, ftl_class_from_dhara(err), + (dhara_sector_t)NAND_FTL_LPN_NONE); return GD5F_ERROR; } - s_cache_dirty = 0; - s_cached_lpn = (dhara_sector_t)-1; - DBG_INFO("NAND FTL formatted, capacity: %lu pages", dhara_map_capacity(&s_map)); return GD5F_OK; } diff --git a/Drivers/BSP/GD5F2GQ5UE/nand_ftl.h b/Drivers/BSP/GD5F2GQ5UE/nand_ftl.h index a89a786f..ee2dd34a 100644 --- a/Drivers/BSP/GD5F2GQ5UE/nand_ftl.h +++ b/Drivers/BSP/GD5F2GQ5UE/nand_ftl.h @@ -19,6 +19,40 @@ extern "C" { #include +/* + * 失败快照(C4):把"最近一次存储失败的归类"从失败点带出来。 + * + * 为什么要单独成一个结构而不是复用 bsp_err_t:调用方需要的不只是失败类, + * 还要能回答"这次失败值不值得再试一次"(重试同一 LPN 会不会又撞同一页)。 + * 故带上 LPN 与 op,便于 run.log/wdev.log 里一条日志说清现场。 + * + * 字段: + * active - 1 = 快照可用(自上次 nand_ftl_fault_clear() 以来发生过失败) + * op - 失败发生的操作(NAND_FTL_OP_*),0 = 无 + * cls - 失败类(bsp_err_t:BSP_ERR_CRC / BSP_ERR_NO_SPACE / BSP_ERR_IO …) + * lpn - 失败涉及的逻辑页号(未知时填 NAND_FTL_LPN_NONE) + * count - 自上次 clear 以来累计的失败次数(饱和不减,用于判"反复撞同一页") + * + * 尺寸:5 × uint32 = **20 B**,无隐式填充(字段全为 4 字节宽,任何编译器布局一致)。 + * 不引入任何动态分配。 + */ +#define NAND_FTL_LPN_NONE ((uint32_t)0xFFFFFFFFu) + +#define NAND_FTL_OP_NONE ((uint32_t)0u) +#define NAND_FTL_OP_MAP_RESUME ((uint32_t)1u) /* disk_initialize 的 dhara_map_resume */ +#define NAND_FTL_OP_READ ((uint32_t)2u) /* ftl_read_page(FatFs 读路径) */ +#define NAND_FTL_OP_FLUSH ((uint32_t)3u) /* ftl_flush_cache(写回路径) */ +#define NAND_FTL_OP_SYNC ((uint32_t)4u) /* disk_ioctl(CTRL_SYNC) 的 map sync */ + +typedef struct { + uint32_t active; /* 1 = 快照可用(自上次 nand_ftl_fault_clear() 以来发生过失败) */ + uint32_t op; /* 失败发生的操作(NAND_FTL_OP_*),0 = 无 */ + int32_t cls; /* 失败类(bsp_err_t:BSP_ERR_CRC / BSP_ERR_NO_SPACE / BSP_ERR_IO …)。 + * 写死 int32_t 宽度:bsp_err_t 是 int,跨编译器布局才稳定 */ + uint32_t lpn; /* 失败涉及的逻辑页号(未知时 = NAND_FTL_LPN_NONE) */ + uint32_t count; /* 自上次 clear 以来累计失败次数(饱和不减,用于判"反复撞同一页") */ +} nand_ftl_fault_t; + /* * 函数功能:初始化 FTL(等价于 disk_initialize(0):SPI 起链 + dhara_map_resume) * 入口参数:无 @@ -53,6 +87,33 @@ int nand_ftl_format(void); */ void nand_ftl_deinit(void); +/* + * 函数功能:取"最近一次存储失败"的跨调用快照(C4,2026-11-04 补还) + * 入口参数:p_out - 快照输出 nand_ftl_fault_t* 可为 NULL(=只查询是否有失败) + * 返回值:无 void(新 API 不把"有没有失败"塞进返回值:本函数是纯查询, + * 判定一律读 p_out->active) + * 限定条件:任何任务上下文可读;**仅诊断用途**,不得据此自动清卷/重格式化 + * 函数说明:为什么需要它——disk_read/disk_write 对 FatFs 只能回 RES_ERROR + * (FatFs 契约),失败类在函数返回时就被抹掉,上层因此无法区分 + * 「页级 TOO_BAD,重格式化可解」与「块池见底 / 映射损坏,重格式化只会把问题 + * 推回去」。本快照把 nand_ftl.c 就地分类的结果留在 RAM 里,供 + * defaultTask 的卷健康探针按类分支(消费点见 storage_bring_up())。 + * ⚠ 多写者:defaultTask 与 testTask 都可能触发写入失败,故快照是"最近一次" + * 语义、非累计账本;并发下可能被另一路覆盖(诊断场景可接受)。 + * 成功路径**不清**它(失败证据要留存到显式 clears),语义同 dbg latch。 + */ +void nand_ftl_fault_get(nand_ftl_fault_t *p_out); + +/* + * 函数功能:清除失败快照(把 active 置 0) + * 入口参数:无 + * 返回值:无 + * 限定条件:仅"已按快照处置完毕"的调用方清除(当前 = defaultTask 卷修复路径) + * 函数说明:不清就会让一次历史失败永久污染后续判定;清早了又会丢掉现场。 + * 调用点因此只有一处,见 storage_bring_up() 的重格式化分支。 + */ +void nand_ftl_fault_clear(void); + #ifdef __cplusplus } #endif diff --git a/docs/任务规划/登记簿.md b/docs/任务规划/登记簿.md index cc4dd1d8..d67cee4a 100644 --- a/docs/任务规划/登记簿.md +++ b/docs/任务规划/登记簿.md @@ -21,7 +21,7 @@ | # | 事项 | 发现 | 证据与现状 | 出处 | |---|---|---|---|---| | dhara 重试自旋残留 | **dhara 重试自旋残留**——`try_recover()` 的 `DHARA_MAX_RETRIES`(8) 自旋在坏块密集时仍可能长时间占用 defaultTask(喂狗点) | 2026-10-02 | 低优先:现有 `vol_lock` + 喂狗节奏缓解;若台架再出"存储侧喂狗饥饿"再排期 | `Lib/dhara/map.c` | -| 可选全序列卷锁 | **可选全序列卷锁**——现卷锁仅覆盖 `syslog` / `cfg` / `lftpd` 三条路径(`vol_lock_wait(0)` try 语义),其它潜在访问点(如 UI 读日志)未纳入 | 2026-10-02 | 待评估:先确认是否真有第二写者;无则关闭本项 | `App/fs/vol_lock.{c,h}` | +| 可选全序列卷锁 | **可选全序列卷锁**——现卷锁仅覆盖 `syslog` / `cfg` / `lftpd` 三条路径(`vol_lock_wait(0)` try 语义),其它潜在访问点(如 UI 读日志)未纳入。⚠ 原本挂在本题下的「dhara 失败类快照(C4)」待还项**已于 2026-11-04 由问题 #79 还清**(`nand_ftl_fault_get/clear` + 探针按类分流,见 `../架构设计/STM32F4-Base存储架构说明.md` §10.4 已闭环段)——本题现只剩**卷锁覆盖面**本身,不再被 C4 阻塞 | 2026-10-02;C4 拆分 2026-11-04 | 待评估:先确认是否真有第二写者;无则关闭本项 | `App/fs/vol_lock.{c,h}` | ## 3. 硬件(V0.1 缺陷 → V0.2 改版项) diff --git a/docs/架构设计/STM32F4-Base存储架构说明.md b/docs/架构设计/STM32F4-Base存储架构说明.md index 9a516045..c0bba68e 100644 --- a/docs/架构设计/STM32F4-Base存储架构说明.md +++ b/docs/架构设计/STM32F4-Base存储架构说明.md @@ -643,7 +643,7 @@ LPN 与原始码的位置**就地分类并打印(`ftl_err_name()` 给可读名 | dhara 错误 | 归类 | 处置上为什么不同 | |---|---|---| -| `ECC` / `TOO_BAD` | `BSP_ERR_CRC` | 位翻转不可纠或页太坏:**是页级问题**,读重试 / 由 dhara 换页,不代表卷坏 | +| `ECC` / `TOO_BAD` | `BSP_ERR_CRC` | 位翻转不可纠或页太坏:**是页级问题**,读重试 / 由 dhara 换页,不代表卷坏(**但见下方 10.4.1:页级失败会因重试语义升级成整卷禁用**) | | `JOURNAL_FULL` / `MAP_FULL` | `BSP_ERR_NO_SPACE` | 介质还健康,是 **journal / 映射层块池见底**:该走容量恢复(`../测试说明/专项验证/存储维护与容量恢复.md`),此时重格式化只是把问题推回去 | | `BAD_BLOCK` / `RECOVER` / `NOT_FOUND` / `CORRUPT_MAP` / 越界 | `BSP_ERR_IO` | 映射或介质**完整性**故障:保卷 + 上报,不许自愈重格式化 | @@ -651,7 +651,80 @@ LPN 与原始码的位置**就地分类并打印(`ftl_err_name()` 给可读名 checkpoint(`DHARA_E_TOO_BAD`)才算真空白卷",本表则保证失败**可读**——两者合起来 才是"别把瞬态毛刺当卷损坏去 `f_mkfs`"(旧行为:单次 I2C/SPI 抖动 → 清 map → 整卷 重格式化 → 静默销毁全部用户数据)。 -- **仍未闭环**:分类目前**只进日志**;`disk_read`/`disk_write` 对 FatFs 仍只能回 `RES_ERROR` - (那是 FatFs 的契约)。要让 defaultTask 的卷健康探针**按类分支**,还需要一个跨调用 - 存活的失败类快照(`nand_ftl_last_error()`,8 B;代价是要先定多写者纪律—— - defaultTask 与 testTask 都会碰存储)→ 即登记簿条目「可选全序列卷锁」的待还项。 +- **已闭环(2026-11-04,问题 #79)**:分类不再只进日志。`nand_ftl.c` 新增跨调用 + 失败快照 `nand_ftl_fault_get()` / `nand_ftl_fault_clear()`(`nand_ftl_fault_t`, + 20 B:`active`/`op`/`cls`/`lpn`/`count`),在 `disk_initialize` 的 resume 失败、 + `ftl_read_page`、`ftl_flush_cache`、`disk_ioctl(CTRL_SYNC)` 四个失败点记账; + defaultTask 的卷健康探针在 `storage_bring_up()` 里读它并按类分流: + `BSP_ERR_NO_SPACE` → **跳过重格式化**(只上报 `storage full`,留待容量恢复), + 其余类 → 才 reformat。多写者纪律:语义是"最近一次"而非累计账本,并发下可能被 + 另一路覆盖(诊断用途可接受),故不加锁、不做权威计数。 + +### 10.4.1 「页级 TOO_BAD」为什么会升级成「整卷禁用」(2026-11-04 台架实证,问题 #79) + +本表上一版只写了「`TOO_BAD` 是页级问题,不代表卷坏」。这句话在**单次读写**的尺度上 +成立,但在**连续重试**的尺度上会误导——台架现场正是这条误解造成的: + +``` +[NAND_FTL] Map resumed: 14094/97504 sectors used ← 映射本身健康 +[ERR][NAND_FTL] Cache flush FAIL: LPN=211 dhara=TOO_BAD class=-7 +[ERR] [FS] volume FAT invalid (fr=1), reformatting... +[ERR] [FS] volume repair failed (fr=1) +[ERR] [FS] f_mount failed (fr=1) ← 3 轮重试 +[ERR] [FS] storage retries exhausted (fr=1), storage disabled +``` + +两个独立的放大机制叠在一起: + +1. **`ftl_flush_cache()` 失败不清 `s_cache_dirty`**(刻意的:写不进去不能假装写成功) + ⇒ 缓存里那一页仍是脏的,**下一次任何存储访问都会重试同一个 LPN**。15 条 + `Cache flush FAIL` 就是这么来的,11 秒内撞 15 次。 +2. **`nand_ftl_format()` 旧实现先 flush、失败即 `return GD5F_ERROR`** ⇒ 本应"销毁 + 映射重建卷"的恢复动作,被**同一页**在第一步挡住,`dhara_map_clear()` 从未执行; + 接着 `f_mkfs` 在**旧映射**上跑,必然失败。每轮启动都重新踩同一条路,故 + **重启不恢复**。 + +- **判据修正**:`TOO_BAD` 仍归类 `BSP_ERR_CRC`(归类没错),但"不代表卷坏"必须加上 + 限定——**不代表介质坏,不代表卷内容好**。单个 LPN 反复写不进去,配合"脏缓存不清 + + 格式化被挡"这两条,足以让整卷在本次启动内不可用。 +- **处置修正**:`nand_ftl_format()` 现在尽力刷一次缓存,失败则**丢弃缓存并继续** + `dhara_map_clear()`——格式化语义本就是销毁整卷映射(见 `nand_ftl.h` 该函数注释 + 「破坏性」),为一份注定作废的脏页挡住恢复动作没有任何收益。⚠ 该"丢弃"权限 + **只在 format 内**;`disk_read`/`disk_write` 的 flush 失败必须继续向上报 `RES_ERROR`。 +- **与 10.4 表的关系**:格式化的**该不该做**由失败类决定(§10.4 快照分流), + 格式化的**做得成做不成**不再被脏缓存劫持(本节)。两者一起才是完整判据。 + +### 10.4.2 上游真凶定案:误标坏块被持久化(2026-11-04 真机闭环,问题 #79) + +10.4.1 的两条放大机制修掉后真机**并未恢复**——`format` 这次确实清了 map,但紧随的 +`Map sync after clear failed: TOO_BAD` 说明**全新的 map 也落不了盘**。逐层加临时 +诊断(prog/erase 原语失败打印 → is_bad 命中打印 → BBT 合并后打印)后定案: + +| 观测 | 结论 | +|---|---| +| `[NAND] BBT scan: 0 bad blocks found` | 出厂无坏块 | +| prog/erase 原语 `[DIAG]` **零输出** | 底层写**全部成功**,TOO_BAD 不来自当次 I/O 失败 | +| `BBT: 129/2048 blocks bad`(合并持久化 BBT 后) | 坏块全部来自**持久化 BBT 的 OR 叠加** | +| 误标是 1016–1142 **连续段**;`Map head=65024`>>6 = **1016** 正中段首 | 不是随机位老化形态 | +| `is_bad hit` 连续命中 1016..1142 | `prepare_head()` 连续 8 次撞坏 → `DHARA_E_TOO_BAD`(journal.c:820-832) | + +**机理链**:某次台架操作(断电 / ST-LINK reset 打在写窗口内)造成**传输层/等忙失败** +→ 旧 `dhara_nand_erase/prog()` 把一切失败都 `dhara_nand_mark_bad()` → +`gd5f2gq5ue_mark_block_bad()` **每次都立即持久化 BBT**(轮转池 + 版本号 + CRC, +不可撤销)→ 误标固化 → 此后每轮启动 journal 写头爬进误标段即 TOO_BAD。这解释了 +「11-03 20:54 还好的、11-04 起每轮必现、重启不恢复」的全部特征。 + +- **修复**:只有芯片明确回 `E_FAIL` / `P_FAIL`(状态位证据)才标坏;传输层/超时 + 只报 `DHARA_E_BAD_BLOCK` 让 dhara 重试,**不标坏**。回归用例 + TC-NANDPORT-207/208/505(主机侧,178 条全绿;把修复回退即见红)。 +- **哨兵**:`gd5f2gq5ue_init()` 解锁块保护(A0h)后补回读校验(与 B0h ECC 同族 + 缺口;实测本板 A0h=0,写保护非本次根因,校验留作哨兵);`gd5f_bbt_load()` 合并后 + 坏块数 > 0 且出厂扫描为 0 时打 `BBT after merge: %lu bad blocks (factory scan + was 0)`——现场一眼区分真坏与误标。 +- **存量误标的处置**:代码修复只防未来。已固化的 129 个误标用恢复动作 A + (`TEST_SUITE_STORAGE` + `ENABLE_STORAGE_RECOVERY_TESTS`,重建 BBT + format) + 清除:`BBT rebuilt and persisted` → `Storage Recovery: 3/3 PASSED` → + 持久化 BBT version=71。产品镜像复烧后 `[FS] FATFS mounted`、 + `[CFG] load: file missing, factory template generated`,0 条 flush FAIL。 + ⚠ 动作 A 语义 = 卷内容全失(旧卷随 map clear 作废),本次台架卷为可弃数据。 + diff --git a/docs/测试说明/专项验证/存储维护与容量恢复.md b/docs/测试说明/专项验证/存储维护与容量恢复.md index 5da8e844..8c05cafd 100644 --- a/docs/测试说明/专项验证/存储维护与容量恢复.md +++ b/docs/测试说明/专项验证/存储维护与容量恢复.md @@ -17,9 +17,25 @@ ## 2. 为什么"容量"需要单独恢复(根因) - 健康满容量 = **96064 pages(187 MB)**(`No valid map found, creating fresh` 分支下的满值)。 + > ⚠ 2026-11-04 台架待核(问题 #79):三次捕获都打 `capacity: 97504 pages = 190 MB` + > (比本基线大 1440 页),且**故障前后数值一致**(不是被扣损的结果)。两种可能未定: + > ① 台架那块板的 `bb_last/bb_current` 与"全新初始化"不同口径;② 本文基线陈旧。 + > 判定方法:台架跑一次动作 B(全片擦除 + 断电重启,走 `dhara_map_clear` 全新分支) + > 后读同一行的 `capacity:`——若仍是 97504,则本文基线应改。(另:本行是**故障本身 + > 无关**的证据,见 架构说明 §10.4.1:容量正常 ⇒ 不是块池耗尽。) + > **2026-11-04 更新(问题 #79)**:当日跑的是动作 A(非 B),BBT 重建 0 坏块 + + > format 后容量仍报 97504=190 MB;且 129 个误标坏块存在期间读数也是 97504 + > (误标只毁 journal 写头,不改本行读数)。基线改判**仍需动作 B**,未跑,本行保留。 - dhara 的坏块计数 `bb_current` / `bb_last` **持久化写在 journal checkpoint 头里** (`Lib/dhara/journal.c` 的 `hdr_set_bb_current/hdr_set_bb_last`,随 checkpoint 一起落盘)。 容量按 `good_blocks = num_blocks − max(bb_last, bb_current) − 1` 递减。 +- **⚠ 还有一处独立于 dhara 计数的坏块持久化**:板级 BBT(`gd5f2gq5ue.c` 的 + `s_bbt[]` 轮转池,`mark_block_bad` 每次**立即落盘、不可撤销**)。2026-11-04 的 + 台架故障根因即在此:传输层失败被旧 `dhara_nand_erase/prog()` 误当坏块证据标坏, + 129 个好块固化进 BBT,journal 写头爬进误标段 → `prepare_head()` 连续 8 次 + `is_bad` → `TOO_BAD` → 整卷禁用(机理见 架构说明 §10.4.2)。这种"误标卷"的**处置 + 就是动作 A**(重扫出厂坏块会覆盖误标并持久化);A 后若 `BBT after merge` 再报 + 非零增量,看的是新增标坏日志 `Erase/Program ... (not a bad block)` 有没有出现。 - `dhara_journal_clear()`(即 `nand_ftl_format()` 走的路)只做 `bb_last = bb_current`,**不归零**—— 所以动作 A 清不掉被扣掉的容量,断电重扫也一样(计数从 checkpoint 读回)。 - 只有让下次启动 `dhara_map_resume` **失败**、走 `dhara_map_clear` 的**全新初始化**分支 @@ -27,6 +43,23 @@ - 记账的触发:一次 `program verify` 失败(日志 `[ERR][NAND_FTL] program verify: data mismatch, blk N`) 就让 dhara 多记一个坏块;破坏性套件(写完擦、坏块注入、格式化)会累积这个数字。 +### 2.1 动作 A 在 2026-11-04 之前是**静默失效**的(问题 #79,已修) + +`nand_ftl_format()` 旧实现的第一步是 `if (ftl_flush_cache() != BSP_OK) return GD5F_ERROR;`, +而 `ftl_flush_cache()` 失败时刻意**不清** `s_cache_dirty`。于是只要脏页落在"写不动的那个 +LPN"上,格式化就在第一道闸门返回,`dhara_map_clear()` **从未执行**——动作 A 名义上跑了, +实际上一个映射都没清,紧随其后的 `f_mkfs` 仍在旧映射上工作、必然失败。 + +- **现场特征**:`Cache flush FAIL: LPN=<固定值> dhara=TOO_BAD` 连打十余条 → + `reformatting...` → `volume repair failed (fr=1)` → `storage disabled`,且**每次启动必现**。 + 看到这条链就别再连续重试动作 A,先确认脏缓存是否正卡在坏页上。 +- **修复后判据**:跑动作 A/B 时若出现新增日志 + `format: dirty cache at LPN= undiscardable, dropping it (map is being destroyed anyway)` + → 说明确实撞上了"脏页卡坏页",但**格式化已经继续执行**(这是预期行为,不是新故障)。 + 其后应能看到 `NAND FTL formatted, capacity: ...`。 +- 语义边界:丢弃脏缓存的权限**只在 format 内**。`disk_read`/`disk_write` 的 flush 失败 + 必须继续向上报 `RES_ERROR`——那是"这次写没落地"的真相。 + ## 3. 操作步骤(以容量恢复为例) 1. `test/board/test_config.h`:取消注释 `TEST_SUITE_STORAGE`; diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index 406a1379..c5d2e03c 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -16,4 +16,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-30 246720 29836 2052 149708 276556 26.4 94336 72.0 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 2026-11-02 247548 29836 2052 149740 277384 26.5 94368 72.0 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 2026-11-03 249112 29836 2052 149996 278948 26.6 94624 72.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 -2026-11-04 253428 30084 2072 150088 283512 27.0 94736 72.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 +2026-11-04 254332 30824 2072 150104 285156 27.2 94752 72.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6236;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 diff --git a/scripts/size_attestation.json b/scripts/size_attestation.json index 52635ad7..4e22dbb1 100644 --- a/scripts/size_attestation.json +++ b/scripts/size_attestation.json @@ -1,25 +1,25 @@ { - "built_at": "2026-11-04T17:59:02+08:00", - "fingerprint": "ab479959f5fd86c89166faf8436055a853db5040350937b93132d527e51f0b30", + "built_at": "2026-11-04T18:58:01+08:00", + "fingerprint": "e144163b2fe1843cedb07864b926cab203f608524605a75fec38dc660b661d29", "fingerprint_files": 1479, "fingerprint_source": "git-index", "gated_regions": [ "RW_IRAM1", "RW_CCM" ], - "git_head": "ad7338365c3ac380c415fb821c464435f81da854", + "git_head": "1f94015434ba809e7e3645316ad3fb4f331824c9", "map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map", - "map_mtime": 1793785414, + "map_mtime": 1793789873, "pct": { - "ER_IROM1": 27.04, + "ER_IROM1": 27.19, "RW_CCM": 87.89, - "RW_IRAM1": 72.28, + "RW_IRAM1": 72.29, "UNINIT_CCM": 18.75 }, "regions": { "ER_IROM1": { "max": 1048576, - "size": 283512 + "size": 285156 }, "RW_CCM": { "max": 65280, @@ -27,7 +27,7 @@ }, "RW_IRAM1": { "max": 131072, - "size": 94736 + "size": 94752 }, "UNINIT_CCM": { "max": 256, @@ -37,10 +37,10 @@ "schema": 2, "scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring", "sizes": { - "code": 253428, - "ro_data": 30084, + "code": 254332, + "ro_data": 30824, "rw_data": 2072, - "zi_data": 150088 + "zi_data": 150104 }, "threshold_pct": 92.0, "worktree_dirt_at_stamp": 0, diff --git a/test/board/storage_test.h b/test/board/storage_test.h index 1fa91c7e..a3b3e041 100644 --- a/test/board/storage_test.h +++ b/test/board/storage_test.h @@ -33,7 +33,7 @@ void storage_test_run(void); /* 存储套件恢复测试(默认关闭):取消注释并编译 STORAGE 套件运行一次, * 即可重建 BBT(清除 TC-STORAGE-03 等注入的坏块)并重新格式化,恢复设备干净态。 * 恢复完成后建议重新注释并编译正常构建。 */ -//#define ENABLE_STORAGE_RECOVERY_TESTS /* 台架已跑过一次恢复(2026-09-20 BBT 回出厂态),保持注释 */ +//#define ENABLE_STORAGE_RECOVERY_TESTS /* #79:2026-11-04 已用本开关清除 129 个误标坏块并 format 成功,保持注释 */ /* 恢复模式的扩展(默认关闭,需与上面 ENABLE_STORAGE_RECOVERY_TESTS 同时打开): * 把 dhara 可见区**整片擦净**(连 journal checkpoint 一起),用于**把被扣掉的容量恢复回来**。 diff --git a/test/host/nand_port/nand_port_test.c b/test/host/nand_port/nand_port_test.c index a7dbd821..17428b36 100644 --- a/test/host/nand_port/nand_port_test.c +++ b/test/host/nand_port/nand_port_test.c @@ -109,6 +109,12 @@ static int s_fail_check_ecc; static int s_fail_program_load; static int s_fail_program_exec; static int s_fail_block_erase; +/* 定向坏页注入(问题 #79):非 0 时,**只有**该物理页的 program_exec 失败 */ +static uint32_t s_fail_program_page = 0u; +/* 传输层失败注入(问题 #79):非 0 时 erase / program_exec 返回传输/超时错误 + * (GD5F_ERROR,芯片**没有**回 E_FAIL/P_FAIL),用于验证"不得标坏块"。 */ +static int s_fail_erase_transport; +static int s_fail_exec_transport; /* ---- SPI 原语(stub/gd5f2gq5ue.h 声明,签名与真实驱动逐字一致) ---- */ @@ -190,6 +196,15 @@ int32_t gd5f_program_exec(uint32_t page_addr) { if (s_fail_program_exec != 0) { return GD5F_PROGRAM_FAIL; } + if (s_fail_exec_transport != 0) { + return GD5F_ERROR; /* 传输/等忙失败:芯片没回 P_FAIL */ + } + /* 定向坏页注入(问题 #79):现场是**某一个 PPA 恒写不进去**,其余页正常。 + * 全局 s_fail_program_exec 无法复现"只有那一页坏"的形态——它连格式化 + * 之后的新写入也一起打死,反而测不出"换个页就能写"这个关键结论。 */ + if ((s_fail_program_page != 0u) && (page_addr == s_fail_program_page)) { + return GD5F_PROGRAM_FAIL; + } /* 严格语义探针:目标页已有非 0xFF 字节 = 双编程(is_free 误判的现场证据) */ for (i = 0; i < FAKE_PAGE_SIZE; i++) { @@ -217,6 +232,9 @@ int32_t gd5f_block_erase(uint32_t block_addr) { if (s_fail_block_erase != 0) { return GD5F_ERASE_FAIL; } + if (s_fail_erase_transport != 0) { + return GD5F_ERROR; /* 传输/等忙失败:芯片没回 E_FAIL */ + } (void)memset(&s_chip[block_addr * FAKE_PAGES_PER_BLOCK][0], 0xFF, (size_t)FAKE_PAGES_PER_BLOCK * FAKE_PAGE_SIZE); return GD5F_OK; @@ -282,6 +300,9 @@ static void fake_chip_reset(void) { s_fail_program_load = 0; s_fail_program_exec = 0; s_fail_block_erase = 0; + s_fail_program_page = 0u; /* 定向坏页注入复位(0 = 不注入,0 号页本身也不允许当靶) */ + s_fail_erase_transport = 0; + s_fail_exec_transport = 0; g_nandtest_err_count = 0; g_nandtest_info_count = 0; g_nandtest_debug_count = 0; @@ -659,9 +680,10 @@ static void tc_prog(void) { CHECK(g_fake_mark_bad_cnt == 1u, "program failure marks exactly one block bad"); CHECK(g_fake_mark_bad_log[0] == 2u, "the block marked bad is the page's own block"); - /* TC-NANDPORT-207:program_load 失败(装载阶段)-> 同样 BAD_BLOCK + 标坏该块, - * 且在 exec 之前就短路(真实芯片:02h 都没成,10h 自然不发) */ - printf("[TC-NANDPORT-207] program_load failure -> BAD_BLOCK + mark bad\n"); + /* TC-NANDPORT-207:program_load 失败(装载阶段)-> BAD_BLOCK,但**不标坏块**。 + * 【#79 修复】装载失败是传输层问题(02h 都没送达芯片),芯片从未回 P_FAIL, + * 把块标坏等于把"线抖了一下"固化成永久坏块。旧实现在此标坏,本用例把守。 */ + printf("[TC-NANDPORT-207] program_load failure -> BAD_BLOCK, NO mark bad\n"); fake_chip_reset(); s_fail_program_load = 1; err = DHARA_E_NONE; @@ -669,8 +691,21 @@ static void tc_prog(void) { "program_load failure -> -1"); CHECK(err == DHARA_E_BAD_BLOCK, "program_load failure -> DHARA_E_BAD_BLOCK"); CHECK(g_fake_program_exec_cnt == 0u, "load failure short-circuits before program_exec"); - CHECK(g_fake_mark_bad_cnt == 1u, "load failure marks exactly one block bad"); - CHECK(g_fake_mark_bad_log[0] == 3u, "the block marked bad is the page's own block"); + CHECK(g_fake_mark_bad_cnt == 0u, "load failure must NOT mark bad (transport, not media)"); + CHECK(fake_bad_unchanged(), "load failure leaves the BBT untouched"); + + /* TC-NANDPORT-208:program_exec 传输/超时失败(芯片未回 P_FAIL)-> 不标坏块。 + * 【#79 核心】真机现场:出厂 0 坏块、底层写全部成功,而持久化 BBT 把 + * 1016–1142 连续段判坏——正是这类失败被旧实现标坏并固化所致。 */ + printf("[TC-NANDPORT-208] program_exec transport failure -> BAD_BLOCK, NO mark bad\n"); + fake_chip_reset(); + s_fail_exec_transport = 1; + err = DHARA_E_NONE; + CHECK(dhara_nand_prog(&s_ut_nand, 4u * 64u, page_buf, &err) == -1, + "program_exec transport failure -> -1"); + CHECK(err == DHARA_E_BAD_BLOCK, "program_exec transport failure -> DHARA_E_BAD_BLOCK"); + CHECK(g_fake_mark_bad_cnt == 0u, "exec transport failure must NOT mark bad"); + CHECK(fake_bad_unchanged(), "exec transport failure leaves the BBT untouched"); } /* ===================== TC-NANDPORT-300..307:dhara_nand_read ===================== */ @@ -867,15 +902,27 @@ static void tc_erase(void) { CHECK(dhara_nand_erase(&s_ut_nand, UT_OOB_BLOCK - 1u, &err) == 0, "erase block == num_blocks - 1 -> 0"); - /* TC-NANDPORT-504:擦除失败 -> BAD_BLOCK + 标坏该块 */ - printf("[TC-NANDPORT-504] erase failure -> BAD_BLOCK + mark bad\n"); + /* TC-NANDPORT-504:芯片回 E_FAIL 的擦除失败 -> BAD_BLOCK + 标坏该块(正确处置) */ + printf("[TC-NANDPORT-504] erase E_FAIL -> BAD_BLOCK + mark bad\n"); fake_chip_reset(); s_fail_block_erase = 1; err = DHARA_E_NONE; CHECK(dhara_nand_erase(&s_ut_nand, 3u, &err) == -1, "block_erase failure -> -1"); CHECK(err == DHARA_E_BAD_BLOCK, "block_erase failure -> DHARA_E_BAD_BLOCK"); - CHECK(g_fake_mark_bad_cnt == 1u, "erase failure marks exactly one block bad"); + CHECK(g_fake_mark_bad_cnt == 1u, "erase E_FAIL marks exactly one block bad"); CHECK(g_fake_mark_bad_log[0] == 3u, "the block marked bad is the one that failed"); + + /* TC-NANDPORT-505:擦除传输/超时失败(芯片未回 E_FAIL)-> 不标坏块。 + * 【#79 核心】这是把 129 个好块误标并持久化的那条路径;标坏是**不可撤销** + * 的持久化动作,只有芯片明确回 E_FAIL 才允许。 */ + printf("[TC-NANDPORT-505] erase transport failure -> BAD_BLOCK, NO mark bad\n"); + fake_chip_reset(); + s_fail_erase_transport = 1; + err = DHARA_E_NONE; + CHECK(dhara_nand_erase(&s_ut_nand, 5u, &err) == -1, "erase transport failure -> -1"); + CHECK(err == DHARA_E_BAD_BLOCK, "erase transport failure -> DHARA_E_BAD_BLOCK"); + CHECK(g_fake_mark_bad_cnt == 0u, "erase transport failure must NOT mark bad"); + CHECK(fake_bad_unchanged(), "erase transport failure leaves the BBT untouched"); } /* ===================== TC-NANDPORT-600..605:is_bad / mark_bad ===================== */ @@ -981,6 +1028,168 @@ static void tc_dhara_scenario(void) { "write + resume scenario: zero double-program violations (scenario guard)"); } +/* ===================== TC-NANDPORT-800..813:问题 #79 回归 ===================== + * + * 现场(2026-11-04 台架,两次独立捕获): + * 探针写 /fschk.tmp → disk_write → ftl_flush_cache 对 LPN=211 反复 TOO_BAD + * → 判卷损坏 → storage_bring_up 调 nand_ftl_format() 想重建 + * → **旧版 format 先 flush 同一脏页、失败即 return** → map 从没被清过 + * → f_mkfs 在旧映射上跑 → 3 轮重试耗尽 → storage disabled(每次启动必现)。 + * + * 本组测三件事: + * ① 脏缓存写不进去时,format **仍然成功**并清掉映射(修复点); + * ② format 后脏缓存被丢弃,不会带着旧 LPN 幽灵继续写(配套不变量); + * ③ 失败快照(C4)真的把"操作 / 失败类 / LPN / 次数"带出函数边界(新能力)。 + * + * 假芯片的失败注入是**按页**的(s_fail_program_exec 是全局开关),而现场是 + * "某个 LPN 的写回恒定失败"。这里用"让 program_exec 恒失败"逼近同一形态: + * 脏页写不进去 → flush 必失败,正是旧版 return 的那个分支。 + */ + +/* 把脏缓存喂进去:直接经 FatFs 叶子的 disk_write 不可行(那会先触发 init), + * 故手工构一个与 disk_write 相同的前置状态:缓存里有一页待写数据。 */ +static void prep_dirty_cache(uint32_t lpn, uint8_t seed) { + fake_chip_reset(); + s_cached_lpn = (dhara_sector_t)lpn; + fake_fill_pattern(s_cache_buf, FTL_PAGE_SIZE, seed); + s_cache_dirty = 1u; +} + +static void tc_format_survives_flush_failure(void) { + uint8_t before[FTL_PAGE_SIZE]; + + /* ---- 前置:一个已建好、里面真有数据的卷(模拟台架"卷原本可用") ---- */ + fake_chip_reset(); + + s_ut_nand.log2_page_size = 11u; + s_ut_nand.log2_ppb = 6u; + s_ut_nand.num_blocks = (unsigned int)FAKE_USABLE_BLOCKS; + dhara_map_init(&s_map, &s_ut_nand, s_page_buf, 4); + memset(s_cache_buf, 0, FTL_PAGE_SIZE); + s_cached_lpn = (dhara_sector_t)-1; + s_cache_dirty = 0; + s_initialized = 1; + + (void)dhara_map_clear(&s_map); + { + dhara_error_t e; + (void)dhara_map_sync(&s_map, &e); + } + + fake_fill_pattern(s_cache_buf, FTL_PAGE_SIZE, 0xA5u); + { + dhara_error_t e; + CHECK(dhara_map_write(&s_map, 0u, s_cache_buf, &e) == 0, + "TC-NANDPORT-800 precondition: sector 0 written"); + } + fake_fill_pattern(s_cache_buf, FTL_PAGE_SIZE, 0x5Au); + { + dhara_error_t e; + CHECK(dhara_map_write(&s_map, 1u, s_cache_buf, &e) == 0, + "TC-NANDPORT-801 precondition: sector 1 written"); + } + CHECK(dhara_map_size(&s_map) == 2u, "TC-NANDPORT-802 precondition: 2 sectors used"); + + /* ---- 关键形态:缓存脏着(脏页写不出去),此时调 format() ---- + * 阶段 A 用"介质整体写不动"逼近最坏情况:program_load 全失败 ⇒ dhara 反复 + * 标坏块 + 重试 8 次后 TOO_BAD,正是 `Cache flush FAIL: LPN=211 … TOO_BAD` + * 那条日志的来源。这一阶段**只断言死锁被打破**,不断言 format 返回 OK—— + * 介质真的全写不动时它本来就落不了盘,报错是诚实的。 */ + memset(s_cache_buf, 0xC3, FTL_PAGE_SIZE); + s_cached_lpn = (dhara_sector_t)211u; /* 与现场 LPN=211 同形 */ + s_cache_dirty = 1u; + memcpy(before, s_cache_buf, FTL_PAGE_SIZE); + + s_fail_program_load = 1; + (void)nand_ftl_format(); + + /* ★ 本单的核心断言:**旧实现到这里 map 一个字节都没清** + * (它在 ftl_flush_cache() 失败那行就 return 了,dhara_map_clear 从未执行)。 + * 新实现必须已经把 map 清空、缓存作废——这正是"恢复动作本身被永久挡住" + * 这条死锁的破点。 */ + CHECK(dhara_map_size(&s_map) == 0u, + "TC-NANDPORT-804 format clears the map even when dirty-cache flush fails (fix for #79)"); + CHECK(s_cache_dirty == 0u, "TC-NANDPORT-805 format discards the dirty cache flag"); + CHECK(s_cached_lpn == (dhara_sector_t)-1, + "TC-NANDPORT-806 format invalidates cached LPN (no ghost page reuse)"); + CHECK(memcmp(s_cache_buf, before, FTL_PAGE_SIZE) == 0, + "TC-NANDPORT-807 format does not fabricate page content"); + + /* ---- 阶段 B:介质可用后(新片上电 = 无坏块积累、map 重新 resume), + * format 必须产出真正可写的空卷。现场对应"下次可写的格式化"。 ---- */ + fake_chip_reset(); /* 同时复位全部失败注入 */ + dhara_map_init(&s_map, &s_ut_nand, s_page_buf, 4); + memset(s_cache_buf, 0, FTL_PAGE_SIZE); + s_cached_lpn = (dhara_sector_t)-1; + s_cache_dirty = 0; + s_initialized = 1; + nand_ftl_fault_clear(); + + { + int rc = nand_ftl_format(); + + CHECK(rc == GD5F_OK, "TC-NANDPORT-808 on recovered media: format returns OK"); + CHECK(dhara_map_size(&s_map) == 0u, + "TC-NANDPORT-808b on recovered media: map is empty after format"); + } + { + dhara_error_t e; + int wrc; + + fake_fill_pattern(s_cache_buf, FTL_PAGE_SIZE, 0x11u); + wrc = dhara_map_write(&s_map, 0u, s_cache_buf, &e); + CHECK(wrc == 0, "TC-NANDPORT-809 after format: new volume accepts writes"); + CHECK(dhara_map_size(&s_map) == 1u, "TC-NANDPORT-810 after format: map counts 1 sector"); + } + + /* ---- 失败快照(C4):新能力,把失败类带出函数边界 ---- */ + nand_ftl_fault_clear(); + { + nand_ftl_fault_t f; + + nand_ftl_fault_get(&f); + CHECK(f.active == 0u, "TC-NANDPORT-811 fault snapshot clear -> inactive"); + CHECK(f.count == 0u, "TC-NANDPORT-811b fault snapshot clear -> count reset"); + } + + /* 造一次可归类的失败:脏缓存 + 写不动 */ + fake_chip_reset(); + memset(s_cache_buf, 0x77, FTL_PAGE_SIZE); + s_cached_lpn = (dhara_sector_t)211u; + s_cache_dirty = 1u; + s_fail_program_load = 1; + { + bsp_err_t cls = ftl_flush_cache(); + nand_ftl_fault_t f; + + CHECK(cls != BSP_OK, "TC-NANDPORT-812a injected flush failure returns an error class"); + + nand_ftl_fault_get(&f); + CHECK(f.active == 1u, "TC-NANDPORT-812 fault snapshot becomes active after a failure"); + CHECK(f.op == NAND_FTL_OP_FLUSH, "TC-NANDPORT-812b snapshot records the failing operation"); + CHECK(f.cls == (int32_t)cls, "TC-NANDPORT-812c snapshot records the failure class"); + CHECK(f.lpn == 211u, "TC-NANDPORT-812d snapshot records the failing LPN"); + CHECK(f.count == 1u, "TC-NANDPORT-812e snapshot counts the failure"); + } + + /* 再失败一次:count 累计(现场是同一页撞 15 次) */ + { + nand_ftl_fault_t f; + + (void)ftl_flush_cache(); + nand_ftl_fault_get(&f); + CHECK(f.count == 2u, "TC-NANDPORT-813 repeated failures accumulate (same-page retry)"); + CHECK(f.lpn == 211u, "TC-NANDPORT-813b repeated failures keep the same LPN"); + } + + s_fail_program_load = 0; + s_fail_program_page = 0u; + nand_ftl_fault_clear(); + s_initialized = 0; + s_cached_lpn = (dhara_sector_t)-1; + s_cache_dirty = 0; +} + /* ===================== main ===================== */ int main(void) { @@ -1023,6 +1232,9 @@ int main(void) { printf("-- dhara map scenario (real dhara, fake chip) --\n"); tc_dhara_scenario(); + printf("-- format survives dirty-cache flush failure + fault snapshot (issue #79) --\n"); + tc_format_survives_flush_failure(); + printf("\n=== NAND Port Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); return (s_failed == 0) ? 0 : 1; -- 2.55.0.windows.2