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

根因(真机定案,问题 #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 1f94015434
commit 5538af829b
11 changed files with 665 additions and 57 deletions
+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