Merge pull request 'fix(nand-ftl): 传输失败不再误标坏块 + format 不再被脏缓存挡死(#79)' (#81) from fix/nand-ftl-format-deadlock into master
Reviewed-on: #81
This commit was merged in pull request #81.
This commit is contained in:
+45
-18
@@ -48,6 +48,7 @@
|
|||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "ff.h"
|
#include "ff.h"
|
||||||
#include "nand_ftl.h"
|
#include "nand_ftl.h"
|
||||||
|
#include "bsp_err.h" /* BSP_ERR_NO_SPACE / BSP_ERR_CRC:卷健康探针按失败类分流(C4) */
|
||||||
#include "net_cfg.h"
|
#include "net_cfg.h"
|
||||||
#include "cfg_file.h"
|
#include "cfg_file.h"
|
||||||
#include "run_par.h" /* 运行参数段落盘(「数据可信」这一项,配置引擎属主 = 本任务) */
|
#include "run_par.h" /* 运行参数段落盘(「数据可信」这一项,配置引擎属主 = 本任务) */
|
||||||
@@ -523,30 +524,56 @@ static FRESULT storage_bring_up(FATFS *fs) {
|
|||||||
FS_VOL_PROBE(hfr); /* 两击确认 */
|
FS_VOL_PROBE(hfr); /* 两击确认 */
|
||||||
}
|
}
|
||||||
if (hfr != FR_OK) {
|
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 判定、
|
/* ⚠ FR_INVALID_NAME 绝不可入本集合:它是路径名语法错误(纯 RAM 判定、
|
||||||
* 不碰盘),与卷健康无关(曾误入 → "无限重格式化"事故) */
|
* 不碰盘),与卷健康无关(曾误入 → "无限重格式化"事故) */
|
||||||
if (hfr == FR_DENIED || hfr == FR_INVALID_OBJECT ||
|
if (hfr == FR_DENIED || hfr == FR_INVALID_OBJECT ||
|
||||||
hfr == FR_INT_ERR || hfr == FR_DISK_ERR ||
|
hfr == FR_INT_ERR || hfr == FR_DISK_ERR ||
|
||||||
hfr == FR_NO_FILE || hfr == FR_NO_PATH ||
|
hfr == FR_NO_FILE || hfr == FR_NO_PATH ||
|
||||||
hfr == FR_NO_FILESYSTEM) {
|
hfr == FR_NO_FILESYSTEM) {
|
||||||
DBG_INFO("[FS] volume FAT invalid (fr=%d), reformatting...",
|
/* 按失败类分流:块池见底 ≠ 介质坏。前者擦卷救不回来,只上报
|
||||||
(int)hfr);
|
* (后续应走容量恢复,见 存储维护与容量恢复.md),后者才 reformat。 */
|
||||||
f_mount(NULL, "", 0);
|
if ((fault.active != 0u) && (fault.cls == (int32_t)BSP_ERR_NO_SPACE)) {
|
||||||
/* 格式化前清空 FTL map 树(同首启路径:新卷从空树写起) */
|
DBG_ERROR("[FS] volume probe failed: FTL out of space, "
|
||||||
(void)nand_ftl_format();
|
"reformat skipped (needs capacity recovery, not mkfs)");
|
||||||
{
|
syslog_post(SYSLOG_SELFTEST, "storage full");
|
||||||
static uint8_t fs_work2[FF_MAX_SS];
|
fr = FR_INT_ERR;
|
||||||
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 {
|
} else {
|
||||||
DBG_ERROR("[FS] volume repair failed (fr=%d)", (int)fr);
|
DBG_INFO("[FS] volume FAT invalid (fr=%d), reformatting...",
|
||||||
syslog_post(SYSLOG_SELFTEST, "storage fault");
|
(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 {
|
} else {
|
||||||
/* 其余错误(写保护/卷锁/参数类)不重格式化——只上报,
|
/* 其余错误(写保护/卷锁/参数类)不重格式化——只上报,
|
||||||
@@ -685,7 +712,7 @@ static void wave_store_drain(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((why == NULL) && (s_wscan_done == 0u)) {
|
if ((why == NULL) && (s_wscan_done == 0u)) {
|
||||||
FRESULT rr;
|
FRESULT rr = FR_OK;
|
||||||
|
|
||||||
fr = f_opendir(&s_wdir, WAVE_ST_DIR);
|
fr = f_opendir(&s_wdir, WAVE_ST_DIR);
|
||||||
if (fr == FR_OK) {
|
if (fr == FR_OK) {
|
||||||
|
|||||||
@@ -8,6 +8,11 @@
|
|||||||
* 2026-07-16 王建锋 创建初始版本
|
* 2026-07-16 王建锋 创建初始版本
|
||||||
* 2026-07-17 王建锋 切换硬件 SPI,修正擦除地址,参考 NuttX 驱动
|
* 2026-07-17 王建锋 切换硬件 SPI,修正擦除地址,参考 NuttX 驱动
|
||||||
* 2026-08-27 补充函数注释(代码规范 V1.0);修正工厂坏块扫描为首页;增加 BBT 持久化
|
* 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);
|
DBG_ERROR("Unlock block protection failed: %d", ret);
|
||||||
return 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...");
|
DBG_INFO("Locating BBT pool...");
|
||||||
ret = gd5f_private_bbt_locate_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);
|
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");
|
DBG_INFO("NAND init OK");
|
||||||
return GD5F_OK;
|
return GD5F_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,31 @@
|
|||||||
* 单一真源 + 两条负数组编译期守卫:越界上界依赖
|
* 单一真源 + 两条负数组编译期守卫:越界上界依赖
|
||||||
* log2_ppb,换料时只改 GD5F_* 而忘改字面量会**静默**
|
* 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 <string.h>
|
||||||
|
#include "nand_ftl.h" /* 本模块公开契约:失败快照类型(C4)与 nand_ftl_* 原型。
|
||||||
|
* 必须自包含——旧版靠调用方先 include 本头,主机侧单测
|
||||||
|
* 直接 #include "nand_ftl.c" 时类型不可见(实测编译错)。 */
|
||||||
#include "gd5f2gq5ue.h"
|
#include "gd5f2gq5ue.h"
|
||||||
#include "ff.h"
|
#include "ff.h"
|
||||||
#include "diskio.h"
|
#include "diskio.h"
|
||||||
@@ -125,10 +146,19 @@ static uint8_t s_copy_buf[FTL_PAGE_SIZE];
|
|||||||
/* 初始化状态 */
|
/* 初始化状态 */
|
||||||
static uint8_t s_initialized;
|
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_flush_cache(void);
|
||||||
static bsp_err_t ftl_read_page(dhara_sector_t lpn);
|
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 获取 */
|
/* 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) {
|
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) */
|
bsp_err_t cls = ftl_class_from_dhara(err); /* 只在失败分支需要(cppcheck variableScope) */
|
||||||
DBG_ERROR("Cache flush FAIL: LPN=%lu dhara=%s class=%d",
|
DBG_ERROR("Cache flush FAIL: LPN=%lu dhara=%s class=%d",
|
||||||
s_cached_lpn, ftl_err_name(err), cls);
|
s_cached_lpn, ftl_err_name(err), cls);
|
||||||
|
ftl_fault_note(NAND_FTL_OP_FLUSH, cls, s_cached_lpn);
|
||||||
return cls;
|
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) */
|
bsp_err_t cls = ftl_class_from_dhara(err); /* 只在失败分支需要(cppcheck variableScope) */
|
||||||
DBG_ERROR("Page read FAIL: LPN=%lu dhara=%s class=%d",
|
DBG_ERROR("Page read FAIL: LPN=%lu dhara=%s class=%d",
|
||||||
lpn, ftl_err_name(err), cls);
|
lpn, ftl_err_name(err), cls);
|
||||||
|
ftl_fault_note(NAND_FTL_OP_READ, cls, lpn);
|
||||||
return cls;
|
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));
|
ret = gd5f_block_erase(ftl_phy_block(b));
|
||||||
if (ret == GD5F_ERASE_FAIL || ret != GD5F_OK) {
|
if (ret != GD5F_OK) {
|
||||||
dhara_nand_mark_bad(n, b);
|
if (ret == GD5F_ERASE_FAIL) {
|
||||||
dhara_set_error(err, DHARA_E_BAD_BLOCK);
|
/* 芯片明确回 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 -1;
|
||||||
}
|
}
|
||||||
return 0;
|
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();
|
gd5f_write_enable();
|
||||||
ret = gd5f_program_load(0, data, FTL_PAGE_SIZE);
|
ret = gd5f_program_load(0, data, FTL_PAGE_SIZE);
|
||||||
if (ret != GD5F_OK) {
|
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);
|
dhara_set_error(err, DHARA_E_BAD_BLOCK);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = gd5f_program_exec(phy_pg);
|
ret = gd5f_program_exec(phy_pg);
|
||||||
if (ret == GD5F_PROGRAM_FAIL || ret != GD5F_OK) {
|
if (ret != GD5F_OK) {
|
||||||
dhara_nand_mark_bad(n, p >> n->log2_ppb);
|
if (ret == GD5F_PROGRAM_FAIL) {
|
||||||
dhara_set_error(err, DHARA_E_BAD_BLOCK);
|
/* 芯片明确回 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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,6 +536,14 @@ DSTATUS disk_initialize(BYTE pdrv) {
|
|||||||
s_cached_lpn = (dhara_sector_t)-1; /* -1 表示缓存未加载;LPN 0 合法,不能用 0 作哨兵 */
|
s_cached_lpn = (dhara_sector_t)-1; /* -1 表示缓存未加载;LPN 0 合法,不能用 0 作哨兵 */
|
||||||
s_cache_dirty = 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;
|
dhara_error_t err = DHARA_E_NONE;
|
||||||
if (dhara_map_resume(&s_map, &err) < 0) {
|
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, "
|
DBG_ERROR("Map resume failed (dhara=%s class=%d), volume kept, "
|
||||||
"storage unavailable", ftl_err_name(err),
|
"storage unavailable", ftl_err_name(err),
|
||||||
ftl_class_from_dhara(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;
|
return STA_NOINIT;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -598,6 +714,8 @@ DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) {
|
|||||||
dhara_error_t err;
|
dhara_error_t err;
|
||||||
if (dhara_map_sync(&s_map, &err) < 0) {
|
if (dhara_map_sync(&s_map, &err) < 0) {
|
||||||
DBG_ERROR("Map sync failed: %d", err);
|
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;
|
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) {
|
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);
|
dhara_map_clear(&s_map);
|
||||||
|
|
||||||
if (dhara_map_sync(&s_map, &err) < 0) {
|
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;
|
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));
|
DBG_INFO("NAND FTL formatted, capacity: %lu pages", dhara_map_capacity(&s_map));
|
||||||
return GD5F_OK;
|
return GD5F_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,40 @@ extern "C" {
|
|||||||
|
|
||||||
#include <stdint.h>
|
#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)
|
* 函数功能:初始化 FTL(等价于 disk_initialize(0):SPI 起链 + dhara_map_resume)
|
||||||
* 入口参数:无
|
* 入口参数:无
|
||||||
@@ -53,6 +87,33 @@ int nand_ftl_format(void);
|
|||||||
*/
|
*/
|
||||||
void nand_ftl_deinit(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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
| # | 事项 | 发现 | 证据与现状 | 出处 |
|
| # | 事项 | 发现 | 证据与现状 | 出处 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| dhara 重试自旋残留 | **dhara 重试自旋残留**——`try_recover()` 的 `DHARA_MAX_RETRIES`(8) 自旋在坏块密集时仍可能长时间占用 defaultTask(喂狗点) | 2026-10-02 | 低优先:现有 `vol_lock` + 喂狗节奏缓解;若台架再出"存储侧喂狗饥饿"再排期 | `Lib/dhara/map.c` |
|
| 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 改版项)
|
## 3. 硬件(V0.1 缺陷 → V0.2 改版项)
|
||||||
|
|
||||||
|
|||||||
@@ -643,7 +643,7 @@ LPN 与原始码的位置**就地分类并打印(`ftl_err_name()` 给可读名
|
|||||||
|
|
||||||
| dhara 错误 | 归类 | 处置上为什么不同 |
|
| 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`),此时重格式化只是把问题推回去 |
|
| `JOURNAL_FULL` / `MAP_FULL` | `BSP_ERR_NO_SPACE` | 介质还健康,是 **journal / 映射层块池见底**:该走容量恢复(`../测试说明/专项验证/存储维护与容量恢复.md`),此时重格式化只是把问题推回去 |
|
||||||
| `BAD_BLOCK` / `RECOVER` / `NOT_FOUND` / `CORRUPT_MAP` / 越界 | `BSP_ERR_IO` | 映射或介质**完整性**故障:保卷 + 上报,不许自愈重格式化 |
|
| `BAD_BLOCK` / `RECOVER` / `NOT_FOUND` / `CORRUPT_MAP` / 越界 | `BSP_ERR_IO` | 映射或介质**完整性**故障:保卷 + 上报,不许自愈重格式化 |
|
||||||
|
|
||||||
@@ -651,7 +651,80 @@ LPN 与原始码的位置**就地分类并打印(`ftl_err_name()` 给可读名
|
|||||||
checkpoint(`DHARA_E_TOO_BAD`)才算真空白卷",本表则保证失败**可读**——两者合起来
|
checkpoint(`DHARA_E_TOO_BAD`)才算真空白卷",本表则保证失败**可读**——两者合起来
|
||||||
才是"别把瞬态毛刺当卷损坏去 `f_mkfs`"(旧行为:单次 I2C/SPI 抖动 → 清 map → 整卷
|
才是"别把瞬态毛刺当卷损坏去 `f_mkfs`"(旧行为:单次 I2C/SPI 抖动 → 清 map → 整卷
|
||||||
重格式化 → 静默销毁全部用户数据)。
|
重格式化 → 静默销毁全部用户数据)。
|
||||||
- **仍未闭环**:分类目前**只进日志**;`disk_read`/`disk_write` 对 FatFs 仍只能回 `RES_ERROR`
|
- **已闭环(2026-11-04,问题 #79)**:分类不再只进日志。`nand_ftl.c` 新增跨调用
|
||||||
(那是 FatFs 的契约)。要让 defaultTask 的卷健康探针**按类分支**,还需要一个跨调用
|
失败快照 `nand_ftl_fault_get()` / `nand_ftl_fault_clear()`(`nand_ftl_fault_t`,
|
||||||
存活的失败类快照(`nand_ftl_last_error()`,8 B;代价是要先定多写者纪律——
|
20 B:`active`/`op`/`cls`/`lpn`/`count`),在 `disk_initialize` 的 resume 失败、
|
||||||
defaultTask 与 testTask 都会碰存储)→ 即登记簿条目「可选全序列卷锁」的待还项。
|
`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. 为什么"容量"需要单独恢复(根因)
|
## 2. 为什么"容量"需要单独恢复(根因)
|
||||||
|
|
||||||
- 健康满容量 = **96064 pages(187 MB)**(`No valid map found, creating fresh` 分支下的满值)。
|
- 健康满容量 = **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 头里**
|
- dhara 的坏块计数 `bb_current` / `bb_last` **持久化写在 journal checkpoint 头里**
|
||||||
(`Lib/dhara/journal.c` 的 `hdr_set_bb_current/hdr_set_bb_last`,随 checkpoint 一起落盘)。
|
(`Lib/dhara/journal.c` 的 `hdr_set_bb_current/hdr_set_bb_last`,随 checkpoint 一起落盘)。
|
||||||
容量按 `good_blocks = num_blocks − max(bb_last, bb_current) − 1` 递减。
|
容量按 `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`,**不归零**——
|
- `dhara_journal_clear()`(即 `nand_ftl_format()` 走的路)只做 `bb_last = bb_current`,**不归零**——
|
||||||
所以动作 A 清不掉被扣掉的容量,断电重扫也一样(计数从 checkpoint 读回)。
|
所以动作 A 清不掉被扣掉的容量,断电重扫也一样(计数从 checkpoint 读回)。
|
||||||
- 只有让下次启动 `dhara_map_resume` **失败**、走 `dhara_map_clear` 的**全新初始化**分支
|
- 只有让下次启动 `dhara_map_resume` **失败**、走 `dhara_map_clear` 的**全新初始化**分支
|
||||||
@@ -27,6 +43,23 @@
|
|||||||
- 记账的触发:一次 `program verify` 失败(日志 `[ERR][NAND_FTL] program verify: data mismatch, blk N`)
|
- 记账的触发:一次 `program verify` 失败(日志 `[ERR][NAND_FTL] program verify: data mismatch, blk N`)
|
||||||
就让 dhara 多记一个坏块;破坏性套件(写完擦、坏块注入、格式化)会累积这个数字。
|
就让 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. 操作步骤(以容量恢复为例)
|
## 3. 操作步骤(以容量恢复为例)
|
||||||
|
|
||||||
1. `test/board/test_config.h`:取消注释 `TEST_SUITE_STORAGE`;
|
1. `test/board/test_config.h`:取消注释 `TEST_SUITE_STORAGE`;
|
||||||
|
|||||||
@@ -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-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-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-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
|
||||||
|
|||||||
|
@@ -1,25 +1,25 @@
|
|||||||
{
|
{
|
||||||
"built_at": "2026-11-04T17:59:02+08:00",
|
"built_at": "2026-11-04T18:58:01+08:00",
|
||||||
"fingerprint": "ab479959f5fd86c89166faf8436055a853db5040350937b93132d527e51f0b30",
|
"fingerprint": "e144163b2fe1843cedb07864b926cab203f608524605a75fec38dc660b661d29",
|
||||||
"fingerprint_files": 1479,
|
"fingerprint_files": 1479,
|
||||||
"fingerprint_source": "git-index",
|
"fingerprint_source": "git-index",
|
||||||
"gated_regions": [
|
"gated_regions": [
|
||||||
"RW_IRAM1",
|
"RW_IRAM1",
|
||||||
"RW_CCM"
|
"RW_CCM"
|
||||||
],
|
],
|
||||||
"git_head": "ad7338365c3ac380c415fb821c464435f81da854",
|
"git_head": "1f94015434ba809e7e3645316ad3fb4f331824c9",
|
||||||
"map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map",
|
"map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map",
|
||||||
"map_mtime": 1793785414,
|
"map_mtime": 1793789873,
|
||||||
"pct": {
|
"pct": {
|
||||||
"ER_IROM1": 27.04,
|
"ER_IROM1": 27.19,
|
||||||
"RW_CCM": 87.89,
|
"RW_CCM": 87.89,
|
||||||
"RW_IRAM1": 72.28,
|
"RW_IRAM1": 72.29,
|
||||||
"UNINIT_CCM": 18.75
|
"UNINIT_CCM": 18.75
|
||||||
},
|
},
|
||||||
"regions": {
|
"regions": {
|
||||||
"ER_IROM1": {
|
"ER_IROM1": {
|
||||||
"max": 1048576,
|
"max": 1048576,
|
||||||
"size": 283512
|
"size": 285156
|
||||||
},
|
},
|
||||||
"RW_CCM": {
|
"RW_CCM": {
|
||||||
"max": 65280,
|
"max": 65280,
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
},
|
},
|
||||||
"RW_IRAM1": {
|
"RW_IRAM1": {
|
||||||
"max": 131072,
|
"max": 131072,
|
||||||
"size": 94736
|
"size": 94752
|
||||||
},
|
},
|
||||||
"UNINIT_CCM": {
|
"UNINIT_CCM": {
|
||||||
"max": 256,
|
"max": 256,
|
||||||
@@ -37,10 +37,10 @@
|
|||||||
"schema": 2,
|
"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",
|
"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": {
|
"sizes": {
|
||||||
"code": 253428,
|
"code": 254332,
|
||||||
"ro_data": 30084,
|
"ro_data": 30824,
|
||||||
"rw_data": 2072,
|
"rw_data": 2072,
|
||||||
"zi_data": 150088
|
"zi_data": 150104
|
||||||
},
|
},
|
||||||
"threshold_pct": 92.0,
|
"threshold_pct": 92.0,
|
||||||
"worktree_dirt_at_stamp": 0,
|
"worktree_dirt_at_stamp": 0,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ void storage_test_run(void);
|
|||||||
/* 存储套件恢复测试(默认关闭):取消注释并编译 STORAGE 套件运行一次,
|
/* 存储套件恢复测试(默认关闭):取消注释并编译 STORAGE 套件运行一次,
|
||||||
* 即可重建 BBT(清除 TC-STORAGE-03 等注入的坏块)并重新格式化,恢复设备干净态。
|
* 即可重建 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 同时打开):
|
/* 恢复模式的扩展(默认关闭,需与上面 ENABLE_STORAGE_RECOVERY_TESTS 同时打开):
|
||||||
* 把 dhara 可见区**整片擦净**(连 journal checkpoint 一起),用于**把被扣掉的容量恢复回来**。
|
* 把 dhara 可见区**整片擦净**(连 journal checkpoint 一起),用于**把被扣掉的容量恢复回来**。
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ static int s_fail_check_ecc;
|
|||||||
static int s_fail_program_load;
|
static int s_fail_program_load;
|
||||||
static int s_fail_program_exec;
|
static int s_fail_program_exec;
|
||||||
static int s_fail_block_erase;
|
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 声明,签名与真实驱动逐字一致) ---- */
|
/* ---- SPI 原语(stub/gd5f2gq5ue.h 声明,签名与真实驱动逐字一致) ---- */
|
||||||
|
|
||||||
@@ -190,6 +196,15 @@ int32_t gd5f_program_exec(uint32_t page_addr) {
|
|||||||
if (s_fail_program_exec != 0) {
|
if (s_fail_program_exec != 0) {
|
||||||
return GD5F_PROGRAM_FAIL;
|
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 误判的现场证据) */
|
/* 严格语义探针:目标页已有非 0xFF 字节 = 双编程(is_free 误判的现场证据) */
|
||||||
for (i = 0; i < FAKE_PAGE_SIZE; i++) {
|
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) {
|
if (s_fail_block_erase != 0) {
|
||||||
return GD5F_ERASE_FAIL;
|
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,
|
(void)memset(&s_chip[block_addr * FAKE_PAGES_PER_BLOCK][0], 0xFF,
|
||||||
(size_t)FAKE_PAGES_PER_BLOCK * FAKE_PAGE_SIZE);
|
(size_t)FAKE_PAGES_PER_BLOCK * FAKE_PAGE_SIZE);
|
||||||
return GD5F_OK;
|
return GD5F_OK;
|
||||||
@@ -282,6 +300,9 @@ static void fake_chip_reset(void) {
|
|||||||
s_fail_program_load = 0;
|
s_fail_program_load = 0;
|
||||||
s_fail_program_exec = 0;
|
s_fail_program_exec = 0;
|
||||||
s_fail_block_erase = 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_err_count = 0;
|
||||||
g_nandtest_info_count = 0;
|
g_nandtest_info_count = 0;
|
||||||
g_nandtest_debug_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_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");
|
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 + 标坏该块,
|
/* TC-NANDPORT-207:program_load 失败(装载阶段)-> BAD_BLOCK,但**不标坏块**。
|
||||||
* 且在 exec 之前就短路(真实芯片:02h 都没成,10h 自然不发) */
|
* 【#79 修复】装载失败是传输层问题(02h 都没送达芯片),芯片从未回 P_FAIL,
|
||||||
printf("[TC-NANDPORT-207] program_load failure -> BAD_BLOCK + mark bad\n");
|
* 把块标坏等于把"线抖了一下"固化成永久坏块。旧实现在此标坏,本用例把守。 */
|
||||||
|
printf("[TC-NANDPORT-207] program_load failure -> BAD_BLOCK, NO mark bad\n");
|
||||||
fake_chip_reset();
|
fake_chip_reset();
|
||||||
s_fail_program_load = 1;
|
s_fail_program_load = 1;
|
||||||
err = DHARA_E_NONE;
|
err = DHARA_E_NONE;
|
||||||
@@ -669,8 +691,21 @@ static void tc_prog(void) {
|
|||||||
"program_load failure -> -1");
|
"program_load failure -> -1");
|
||||||
CHECK(err == DHARA_E_BAD_BLOCK, "program_load failure -> DHARA_E_BAD_BLOCK");
|
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_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_cnt == 0u, "load failure must NOT mark bad (transport, not media)");
|
||||||
CHECK(g_fake_mark_bad_log[0] == 3u, "the block marked bad is the page's own block");
|
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 ===================== */
|
/* ===================== 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,
|
CHECK(dhara_nand_erase(&s_ut_nand, UT_OOB_BLOCK - 1u, &err) == 0,
|
||||||
"erase block == num_blocks - 1 -> 0");
|
"erase block == num_blocks - 1 -> 0");
|
||||||
|
|
||||||
/* TC-NANDPORT-504:擦除失败 -> BAD_BLOCK + 标坏该块 */
|
/* TC-NANDPORT-504:芯片回 E_FAIL 的擦除失败 -> BAD_BLOCK + 标坏该块(正确处置) */
|
||||||
printf("[TC-NANDPORT-504] erase failure -> BAD_BLOCK + mark bad\n");
|
printf("[TC-NANDPORT-504] erase E_FAIL -> BAD_BLOCK + mark bad\n");
|
||||||
fake_chip_reset();
|
fake_chip_reset();
|
||||||
s_fail_block_erase = 1;
|
s_fail_block_erase = 1;
|
||||||
err = DHARA_E_NONE;
|
err = DHARA_E_NONE;
|
||||||
CHECK(dhara_nand_erase(&s_ut_nand, 3u, &err) == -1, "block_erase failure -> -1");
|
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(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");
|
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 ===================== */
|
/* ===================== 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)");
|
"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 ===================== */
|
/* ===================== main ===================== */
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
@@ -1023,6 +1232,9 @@ int main(void) {
|
|||||||
printf("-- dhara map scenario (real dhara, fake chip) --\n");
|
printf("-- dhara map scenario (real dhara, fake chip) --\n");
|
||||||
tc_dhara_scenario();
|
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",
|
printf("\n=== NAND Port Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||||
s_passed, s_total, s_failed);
|
s_passed, s_total, s_failed);
|
||||||
return (s_failed == 0) ? 0 : 1;
|
return (s_failed == 0) ? 0 : 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user