fix(nand-ftl): 传输失败不再误标坏块 + format 不再被脏缓存挡死(#79)
CI / 静态门禁 (pre-commit) (push) Failing after 42s
CI / 主机侧单测 (MSVC) (push) Failing after 43s
CI / 静态门禁 (pre-commit) (pull_request) Failing after 45s
CI / 主机侧单测 (MSVC) (pull_request) Failing after 45s

根因(真机定案,问题 #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
  文档通过
This commit is contained in:
2026-11-04 18:16:36 +08:00
parent 2b848c4a0b
commit 0d21d9bf04
11 changed files with 665 additions and 57 deletions
+45 -18
View File
@@ -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) {
+52
View File
@@ -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;
}
+162 -12
View File
@@ -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 <string.h>
#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;
}
+61
View File
@@ -19,6 +19,40 @@ extern "C" {
#include <stdint.h>
/*
* 失败快照(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
+1 -1
View File
@@ -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 改版项)
@@ -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 作废),本次台架卷为可弃数据。
@@ -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=<n> 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`;
+1 -1
View File
@@ -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 254648 30152 2072 150088 284800 27.2 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 255552 30892 2072 150104 286444 27.3 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
1 date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
16 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
17 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
18 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
19 2026-11-04 254648 255552 30152 30892 2072 150088 150104 284800 286444 27.2 27.3 94736 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=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 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
+11 -11
View File
@@ -1,25 +1,25 @@
{
"built_at": "2026-11-04T16:20:20+08:00",
"fingerprint": "6b401a3591de1df1ce9d532c275648650896fb04a175f90b3a0caa576c6c6359",
"built_at": "2026-11-04T18:51:04+08:00",
"fingerprint": "45f08600162f2c96e03a20723fea37b99e4ac1da212d8925b8541926e07ebc7c",
"fingerprint_files": 1479,
"fingerprint_source": "git-index",
"gated_regions": [
"RW_IRAM1",
"RW_CCM"
],
"git_head": "10322208fd0908b2ef04292833d4bc2b417e3534",
"git_head": "f979b394743c5a82a23beb19607096c02f20f63e",
"map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map",
"map_mtime": 1793780256,
"map_mtime": 1793789451,
"pct": {
"ER_IROM1": 27.16,
"ER_IROM1": 27.32,
"RW_CCM": 87.89,
"RW_IRAM1": 72.28,
"RW_IRAM1": 72.29,
"UNINIT_CCM": 18.75
},
"regions": {
"ER_IROM1": {
"max": 1048576,
"size": 284800
"size": 286444
},
"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": 254648,
"ro_data": 30152,
"code": 255552,
"ro_data": 30892,
"rw_data": 2072,
"zi_data": 150088
"zi_data": 150104
},
"threshold_pct": 92.0,
"worktree_dirt_at_stamp": 0,
+1 -1
View File
@@ -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 一起),用于**把被扣掉的容量恢复回来**。
+220 -8
View File
@@ -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;