Files
STM32F4-Base/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c
T
super_agent_bot 0d21d9bf04
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
fix(nand-ftl): 传输失败不再误标坏块 + format 不再被脏缓存挡死(#79)
根因(真机定案,问题 #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
  文档通过
2026-11-04 18:51:24 +08:00

804 lines
35 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:NAND FTL 适配层(dhara + FatFS diskio 胶水)
* 模块功能:实现 dhara NAND HAL 和 FatFS 磁盘 I/O,连接 GD5F2GQ5UE 底层驱动
* 适用平台:STM32F407ZGT6
* 作者:王建锋
* 创建日期:2026-07-20
* 修改记录:
* 2026-07-20 王建锋 创建初始版本
* 2026-09-21 resume 失败按错误分类:仅真空白判新卷,其余失败
* 返回 mount 层(禁止静默清 map 触发整卷重格式化)
* 2026-10-02 王建锋 resume 成功后增打 journal head/tail/epoch:复盘
* "复位后所有写入丢失"时 sectors used 不足以判定
* journal 回退(覆盖写不改 count),head/tail/epoch 是
* 唯一能暴露 resume 锚错 checkpoint 的证据
* 2026-10-15 王建锋 落实 dhara 移植层两条硬约束:① is_free 改**整页扫描**——
* 旧实现只看页首 64 B,会把"页首 0xFF、后段有数据"的
* 已编程页误判空闲,dhara 遂往已编程页再编程 = 数据丢失
* (页首有数据的绝大多数页走快路径,成本与旧实现同价);
* ② HAL 六个入口全部加几何上界守卫——越界页/块号(含
* DHARA_PAGE_NONE)旧会被地址位截断成真实页 → 静默读写
* 错页,现报 DHARA_E_CORRUPT_MAP(不能用 BAD_BLOCK:会
* 误标无辜好块;不能用 RECOVER:会触发回收);
* ③ 补主机侧移植层纯逻辑单测 test/host/nand_port/;
* ④ 几何字面量收成 FTL_LOG2_PAGE_SIZE / FTL_LOG2_PPB
* 单一真源 + 两条负数组编译期守卫:越界上界依赖
* 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"
#include "nand.h"
#include "map.h"
#include "bsp_err.h" /* C2:把 dhara 错误归类成跨模块可判定的失败类 */
/* FTL 分区起始地址(块 0,偏移 0,占用全部 256MB) */
#define FTL_FATFS_OFFSET 0U
/* 调试输出配置 */
/* 深调用链模块(FatFS → 本 FTL → dhara → NAND 是 defaultTask 最坏链的主干):
* 日志行缓冲收到 128 B(默认 192)。本模块最长行 "[2026-10-10 08:00:00][ERR][NAND_FTL]
* Cache flush failed: LPN=4294967295 err=-2147483648" ≈ 84 B < 128 → 不截断。
* 核算 scripts/stack_report.py,见 docs/内存管理/RAM使用分析与优化方案.md */
#define DBG_LINE_BUF_MAX 128U
#define DBG_TAG "[NAND_FTL]"
#include "dbg_log.h"
/* 私有宏定义区 */
#define FTL_PAGE_SIZE GD5F_PAGE_SIZE
#define FTL_SECTORS_PER_PAGE (FTL_PAGE_SIZE / 512)
#define FTL_START_BLOCK (FTL_FATFS_OFFSET / GD5F_BLOCK_SIZE)
#define FTL_NUM_BLOCKS (GD5F_TOTAL_BLOCKS - FTL_START_BLOCK)
#define FTL_START_PAGE (FTL_START_BLOCK * GD5F_PAGES_PER_BLOCK)
/* dhara 几何字面量的**单一真源**(TRAP-GD5F-07 遗留 3 的收口):disk_initialize 与
* 下方守卫都取这两个宏,不再各写一遍 11 / 6。 */
#define FTL_LOG2_PAGE_SIZE 11u
#define FTL_LOG2_PPB 6u
/* 编译期守卫:`ftl_page_in_range()` 的越界上界 = `num_blocks << log2_ppb`,
* **完全依赖 log2_ppb**。将来换料(例如 128 页/块的兼容片)只改了 GD5F_* 常量
* 而忘了同步这里的字面量,守卫会静默算错——偏大就是越界守卫失效,正是它要防的
* 那一类错误,而且不报任何错。下面两条把"字面量 == GD5F 常量的 log2"钉死,
* 不一致直接编译错。负数组 typedef 是 AC5 下的项目惯用法(AC5 不支持 C11
* _Static_assert),同本目录 gd5f2gq5ue.h 的 DMA 契约守卫。 */
typedef char ftl_geometry_log2_page_size_check_t[
(FTL_PAGE_SIZE == (1 << FTL_LOG2_PAGE_SIZE)) ? 1 : -1];
typedef char ftl_geometry_log2_ppb_check_t[
(GD5F_PAGES_PER_BLOCK == (1 << FTL_LOG2_PPB)) ? 1 : -1];
/* dhara 逻辑地址到物理地址转换 */
static inline uint32_t ftl_phy_block(dhara_block_t b) { return b + FTL_START_BLOCK; }
static inline uint32_t ftl_phy_page(dhara_page_t p) { return p + FTL_START_PAGE; }
/*
* dhara 移植层硬约束(2026-10-15):移植层地址守卫
*
* 旧实现把 dhara 传来的页/块号直接加偏移送进 SPI 命令,**不做任何上界校验**。
* 越界页号(含 dhara 的"无页"哨兵 DHARA_PAGE_NONE = 0xffffffff,journal.h:46)
* 经 ftl_phy_page() 相加后会被芯片地址位截断成某个**真实存在的页** → 静默读/写
* 错页,比报错严重得多(数据错而传输"成功",与 CCM-DMA 那条同族)。
*
* 为什么报 DHARA_E_CORRUPT_MAP 而不是报告原建议的 DHARA_E_UNSUPPORTED:
* 本仓裁剪版 dhara 的错误枚举(Lib/dhara/error.h:20-31)里**没有**
* E_UNSUPPORTED,写上去直接编译失败。
* 为什么不用 DHARA_E_BAD_BLOCK:journal.c:904 的 recover_from 只对 BAD_BLOCK
* 走"标坏块 + 跳块重试"——对一个凭空的地址标坏块 = 毁掉一个无辜的好块。
* 为什么不会触发静默清卷:map.c:275 的 try_recover 只对 E_RECOVER 启动回收,
* 其余错误一律原样上抛(教训:单次瞬态毛刺不得销毁整卷数据)。
* is_free / is_bad 没有错误通道 → 一律保守取值(判非空 / 判坏),与同族
* 口径一致:宁可少用一个块,不可把不可信的内容当有效。
*/
static int ftl_page_in_range(const struct dhara_nand *n, dhara_page_t p) {
if ((n == NULL) || (n->num_blocks == 0u)) {
return 0;
}
return p < ((dhara_page_t)n->num_blocks << n->log2_ppb);
}
static int ftl_block_in_range(const struct dhara_nand *n, dhara_block_t b) {
if ((n == NULL) || (n->num_blocks == 0u)) {
return 0;
}
return b < n->num_blocks;
}
/* ======================== 私有变量 ======================== */
/* dhara 核心数据结构 */
static struct dhara_nand s_nand;
static struct dhara_map s_map;
static uint8_t s_page_buf[FTL_PAGE_SIZE]; /* dhara 内部页缓冲区 */
/* 页面缓存(FatFS 读写缓存) */
static uint8_t s_cache_buf[FTL_PAGE_SIZE];
static dhara_sector_t s_cached_lpn;
static uint8_t s_cache_dirty;
/* 拷贝临时缓冲区 */
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 获取 */
/* ======================== dhara 错误的可读名与失败类 ======================== */
/*
* 函数功能:把 dhara 错误枚举翻成短字符串(**本仓 dhara 没编 error.c,没有 dhara_strerror()**,
* 用它会链接失败,故自己给一张小表)
* 入口参数:err - dhara 错误枚举值
* 返回值:静态字符串常量("NONE" / "ECC" / "MAP_FULL" ...,越界返回 "OOB")
* 限定条件:只用于日志,不得用于判等
* 函数说明:以前这里只打 `err=%d`——现场拿到一串数字还得翻 Lib/dhara/error.h 才知道是什么,
* 存储故障又大多是"看完日志就没机会复现"的那类,可读名是排障成本最低的改法。
*/
static const char *ftl_err_name(dhara_error_t err) {
static const char *const s_names[] = {
"NONE", "BAD_BLOCK", "ECC", "TOO_BAD", "RECOVER",
"JOURNAL_FULL", "NOT_FOUND", "MAP_FULL", "CORRUPT_MAP",
};
if ((unsigned int)err < (sizeof(s_names) / sizeof(s_names[0]))) {
return s_names[err];
}
return "OOB";
}
/*
* 函数功能:把 dhara 错误归类成统一失败类(C2)
* 入口参数:err - dhara 错误枚举值
* 返回值:BSP_ERR_CRC(位翻转不可纠 / 页太坏);BSP_ERR_NO_SPACE(journal 或映射层预算
* 耗尽);其余(坏块 / 找回失败 / 映射损坏 / 越界)→ BSP_ERR_IO
* 限定条件:入参应是**失败**的 err(DHARA_E_NONE 会得 BSP_OK)
* 函数说明:分这三类的理由是它们在**处置上真的不同**,不是为了码好看:
* · ECC/TOO_BAD → 该重试读或标页坏,**不是**卷的问题;
* · JOURNAL_FULL/MAP_FULL → 介质还健康,是**块池/日志预算见底**,此时重格式化只是
* 把问题推回去(教训正是"别把非空白错误当新卷清 map"),该走容量恢复流程;
* · BAD_BLOCK/CORRUPT_MAP/RECOVER → 映射或介质完整性故障,该保卷 + 上报,不许自愈重格式化。
* 本函数先把这三类**打出来**(就地日志,最早且唯一同时知道 LPN 与原始码的位置);
* 要让 defaultTask 的卷健康探针按类分支,还需要一个跨调用存活的快照(C4 待还)。
*/
static bsp_err_t ftl_class_from_dhara(dhara_error_t err) {
switch (err) {
case DHARA_E_NONE:
return BSP_OK;
case DHARA_E_ECC:
case DHARA_E_TOO_BAD:
return BSP_ERR_CRC;
case DHARA_E_JOURNAL_FULL:
case DHARA_E_MAP_FULL:
return BSP_ERR_NO_SPACE;
default:
return BSP_ERR_IO;
}
}
/* ======================== 失败快照(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) {
if (!s_cache_dirty) {
return BSP_OK;
}
dhara_error_t err;
if (dhara_map_write(&s_map, s_cached_lpn, s_cache_buf, &err) < 0) {
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;
}
s_cache_dirty = 0;
return BSP_OK;
}
static bsp_err_t ftl_read_page(dhara_sector_t lpn) {
dhara_error_t err;
if (dhara_map_read(&s_map, lpn, s_cache_buf, &err) < 0) {
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;
}
s_cached_lpn = lpn;
s_cache_dirty = 0;
return BSP_OK;
}
/* ======================== dhara NAND HAL ======================== */
int dhara_nand_is_bad(const struct dhara_nand *n, dhara_block_t b) {
if (ftl_block_in_range(n, b) == 0) {
return 1; /* 越界块:保守判坏,让 dhara 跳过 */
}
return gd5f2gq5ue_is_block_bad(ftl_phy_block(b));
}
void dhara_nand_mark_bad(const struct dhara_nand *n, dhara_block_t b) {
if (ftl_block_in_range(n, b) == 0) {
return; /* 越界块:绝不标坏,避免毁掉无辜好块 */
}
gd5f2gq5ue_mark_block_bad(ftl_phy_block(b));
}
int dhara_nand_erase(const struct dhara_nand *n, dhara_block_t b, dhara_error_t *err) {
int ret;
if (ftl_block_in_range(n, b) == 0) {
dhara_set_error(err, DHARA_E_CORRUPT_MAP);
return -1;
}
ret = gd5f_block_erase(ftl_phy_block(b));
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;
}
int dhara_nand_prog(const struct dhara_nand *n, dhara_page_t p, const uint8_t *data, dhara_error_t *err) {
int ret;
uint32_t phy_pg;
if ((ftl_page_in_range(n, p) == 0) || (data == NULL)) {
dhara_set_error(err, DHARA_E_CORRUPT_MAP);
return -1;
}
phy_pg = ftl_phy_page(p);
gd5f_write_enable();
ret = gd5f_program_load(0, data, FTL_PAGE_SIZE);
if (ret != GD5F_OK) {
/* 【#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_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;
}
/* 编程正确性以 P_FAIL 状态位(gd5f_program_exec 内检查)+ 读时 ECC +
* journal 读时门(magic/CRC)为准;数据路径不做整页回读比对——
* 2026-09-20 曾加回读校验(GD5F_PROG_VERIFY),2026-09-21 经评估整体
* 移除(依据见 docs/内存管理/存储栈RAM优化调研.md 与 GD5F Trap Records TRAP-GD5F-05) */
return 0;
}
/*
* dhara 移植层硬约束(2026-10-15):整页扫描
*
* 旧实现只看页首 64 B。dhara 的 journal 用 is_free 判断"检查点组是否未编程"
* (journal.c:416 cp_free、journal.c:520 找下一空闲页),其正确性前提是
* **is_free 答"空闲"的页必须真的全 0xFF**——journal.c:405-414 的注释论证了
* "只要组内被编程过的页含非 0xFF 字节就能识别出来",可若那一页恰好页首 64 B
* 是 0xFF、后段才有数据,旧实现就会误判空闲。误判的后果不是报错,而是
* **往已编程的页再编程一次** → 位碰撞、journal/map 指向垃圾 = 数据丢失。
* 本卷上跑的是 FatFS 数据页,"页首一段 0xFF、后段有数据"完全常见(刚格式化的
* 卷、稀疏文件块、日志尾部),所以必须整页判。
*
* 成本处理:先查页首 64 B——页首就有数据的页(绝大多数已编程页)当场返回非空,
* 与旧实现同价;只有"目前读到的全是 0xFF"的页才继续扫后段。分块用栈上 64 B
* 缓冲,不新增 2 KB 静态 RAM(页缓冲三块不可合并的论证见
* docs/内存管理/存储栈RAM优化调研.md §5)。
*/
int dhara_nand_is_free(const struct dhara_nand *n, dhara_page_t p) {
uint8_t buf[64];
uint32_t phy_pg;
uint32_t off;
uint32_t i;
if (ftl_page_in_range(n, p) == 0) {
return 0; /* 越界页号:保守判非空 */
}
phy_pg = ftl_phy_page(p);
if (gd5f_page_read(phy_pg) != GD5F_OK) {
return 0;
}
for (off = 0; off < FTL_PAGE_SIZE; off += sizeof(buf)) {
if (gd5f_read_from_cache((uint16_t)off, buf, sizeof(buf)) != GD5F_OK) {
/* 同族(2026-10-03):读失败 → buf 可能为陈旧/垃圾内容,
* 保守判为非空(块不可复用),避免把"没读到内容的块"判成空闲。 */
return 0;
}
for (i = 0; i < sizeof(buf); i++) {
if (buf[i] != 0xFFu) {
return 0; /* 页首命中即返回 = 快路径,不读后段 */
}
}
}
return 1;
}
int dhara_nand_read(const struct dhara_nand *n, dhara_page_t p, size_t offset, size_t length, uint8_t *data, dhara_error_t *err) {
uint32_t phy_pg;
if ((ftl_page_in_range(n, p) == 0) || (data == NULL) ||
(offset >= FTL_PAGE_SIZE) || (length > (FTL_PAGE_SIZE - offset))) {
dhara_set_error(err, DHARA_E_CORRUPT_MAP);
return -1;
}
phy_pg = ftl_phy_page(p);
if (gd5f_page_read(phy_pg) != GD5F_OK) {
dhara_set_error(err, DHARA_E_ECC);
return -1;
}
if (gd5f_check_ecc() != GD5F_OK) {
dhara_set_error(err, DHARA_E_ECC);
return -1;
}
/* 同族(2026-10-03):read_from_cache 返回值必须检查——失败时 data 内容
* 不可信,原实现仍返回 0(成功),会把未读到的数据当有效数据交给上层。 */
if (gd5f_read_from_cache((uint16_t)offset, data, length) != GD5F_OK) {
dhara_set_error(err, DHARA_E_ECC);
return -1;
}
return 0;
}
/*
* dhara 移植层硬约束(2026-10-15):src / dst 双端守卫
*
* 报告原判断"dhara map 在回收/擦除路径会用 DHARA_NO_PAGE 试探"经逐点核对
* **不成立**:本仓裁剪版没有 DHARA_NO_PAGE,哨兵叫 DHARA_PAGE_NONE
* (journal.h:46),而 map.c 三处 dhara_journal_copy 调用点都已先挡掉它
* (map.c:260 挡 pad_queue 的 p、map.c:434/440 挡 alt_page、map.c:242 的 src
* 来自 trace_path 的真实页)。但"移植层不做任何上界校验"这个风险是真的:
* 哨兵一旦从别处漏进来(未来改动、被污染的 meta),ftl_phy_page() 会把
* 0xffffffff 截断成某个真实页 → **静默搬走无辜数据**。故 src/dst 各挡一次,
* dst 越界尤其致命——那是一次写入。
*/
int dhara_nand_copy(const struct dhara_nand *n, dhara_page_t src, dhara_page_t dst, dhara_error_t *err) {
if ((ftl_page_in_range(n, src) == 0) || (ftl_page_in_range(n, dst) == 0)) {
dhara_set_error(err, DHARA_E_CORRUPT_MAP);
return -1;
}
if (dhara_nand_read(n, src, 0, FTL_PAGE_SIZE, s_copy_buf, err) < 0) {
return -1;
}
if (dhara_nand_prog(n, dst, s_copy_buf, err) < 0) {
return -1;
}
return 0;
}
/* ======================== FatFS 磁盘 I/O ======================== */
DSTATUS disk_initialize(BYTE pdrv) {
if (pdrv != 0) {
return STA_NOINIT;
}
if (s_initialized) {
return 0;
}
DBG_INFO("Initializing NAND FTL...");
/* DMA 可达性三层防线——启动期地址自检(2026-10-12):本文件三个页缓冲是 SPI1 DMA 路径上
* 唯一的**静态**缓冲(dhara_map_read/write → dhara_nand_read/prog/copy,
* 2048 B > GD5F_DMA_THRESHOLD;s_page_buf 页内虽只攒 132 B 元数据,
* 但编程的是**整页**(journal.c:992),传输长度同为 2048 B、同样走 DMA,
* 一并守住防将来越窗)。
* DMA1/DMA2 不可达 CCM——若 scatter 改动/段放置把它们挪进 CCM,DMA 会
* **静默不落地**(数据错而传输"成功")。缓冲地址链接期才确定,armcc
* 无法编译期断言(见 gd5f2gq5ue.h 的窗口常量注释),故在 init 期自检:
* 任一缓冲越出 DMA 白名单窗口 → 上报并拒绝挂载(存储降级、不挂死,
* 与"保留卷、上报自检"形态一致)。每次传输的运行期守卫
* (gd5f_read_from_cache/gd5f_program_load 内)保留作第二层兜底。 */
if ((gd5f_dma_buf_ok(s_cache_buf, FTL_PAGE_SIZE) == 0) ||
(gd5f_dma_buf_ok(s_copy_buf, FTL_PAGE_SIZE) == 0) ||
(gd5f_dma_buf_ok(s_page_buf, FTL_PAGE_SIZE) == 0)) {
DBG_ERROR("FTL page buffer not DMA-reachable (cache=0x%08lX), "
"storage refused", (unsigned long)(uintptr_t)s_cache_buf);
return STA_NOINIT;
}
s_nand.log2_page_size = FTL_LOG2_PAGE_SIZE;
s_nand.log2_ppb = FTL_LOG2_PPB;
/* dhara 可见块数 = 总块数 - 保留块池(BBT 存储区不交给 dhara,避免被擦写) */
s_nand.num_blocks = gd5f_get_usable_blocks();
dhara_map_init(&s_map, &s_nand, s_page_buf, 4);
memset(s_cache_buf, 0, FTL_PAGE_SIZE);
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) {
if (err == DHARA_E_TOO_BAD) {
/* 扫描全程 I/O 干净且无 checkpoint = 真空白卷(出厂首启 /
* 显式格式化后);失败的 resume 已把 journal 复位为空态 →
* 按新卷处理,由上层自动建卷(此处 dhara_map_clear 为空操作:
* resume 失败时 count 已清零,map 本已为空)。 */
DBG_INFO("No valid map found (dhara=%s), creating fresh", ftl_err_name(err));
dhara_map_clear(&s_map);
} else {
/* (2026-09-21 审计 P0 修复):非"空白"错误(ECC /
* BAD_BLOCK / CORRUPT_MAP / 瞬态 I/O 等)= 真实完整性故障——
* 芯片上 journal 可能仍持有有效 checkpoint。旧行为把任何失败
* 当新卷清 map,上层随即整卷重格式化 → 单次瞬态毛刺即静默销毁
* 全部用户数据。现把失败返回给 mount 层:f_mount 失败 →
* defaultTask 自检上报 "filesystem fault" 并保留卷;瞬态故障
* 由主循环重试/下次上电重新 resume;重格式化只走显式命令
* (nand_ftl_format)。 */
DBG_ERROR("Map resume failed (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 {
DBG_INFO("Map resumed: %lu/%lu sectors used",
dhara_map_size(&s_map), dhara_map_capacity(&s_map));
/* journal 头/尾/epoch 一并打印:2026-10-02 台架复盘"复位后写入全部
* 丢失"时,只看 sectors used 判断不出 journal 是否回退(覆盖写场景
* count 不变),必须看 head/tail/epoch 才能发现 resume 锚错了
* checkpoint(详见 Lib/dhara/journal.c 的 2026-10-02 本地修复注释)。
* 纯内存字段、零 I/O,保留作长期可观测性。 */
DBG_INFO("Map head=%lu tail=%lu epoch=%u",
(unsigned long)s_map.journal.head,
(unsigned long)s_map.journal.tail,
(unsigned)s_map.journal.epoch);
}
}
s_initialized = 1;
DBG_INFO("NAND FTL ready (capacity: %lu pages = %lu MB)",
dhara_map_capacity(&s_map),
(dhara_map_capacity(&s_map) * FTL_PAGE_SIZE) / (1024 * 1024));
return 0;
}
int nand_ftl_init(void)
{
DSTATUS sta = disk_initialize(0);
return (sta == 0) ? 0 : -1;
}
void nand_ftl_deinit(void)
{
/* 复位 FTL 状态,使下次 nand_ftl_init() 真正重新走 dhara_map_resume,
用于测试"掉电恢复"与"坏块注入后重建"场景(模拟设备重启) */
s_initialized = 0;
s_cached_lpn = (dhara_sector_t)-1;
s_cache_dirty = 0;
}
DSTATUS disk_status(BYTE pdrv) {
if (pdrv != 0) {
return STA_NOINIT;
}
if (!s_initialized) {
return STA_NOINIT;
}
return 0;
}
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) {
if (pdrv != 0 || !buff) {
return RES_PARERR;
}
if (!s_initialized && disk_initialize(pdrv) != 0) {
return RES_NOTRDY;
}
while (count > 0) {
dhara_sector_t lpn = (dhara_sector_t)(sector / FTL_SECTORS_PER_PAGE);
uint32_t offset = (uint32_t)(sector % FTL_SECTORS_PER_PAGE) * 512;
uint32_t batch = FTL_SECTORS_PER_PAGE - (sector % FTL_SECTORS_PER_PAGE);
if (batch > count) {
batch = count;
}
if (lpn != s_cached_lpn) {
if (ftl_flush_cache() != BSP_OK) {
return RES_ERROR;
}
if (ftl_read_page(lpn) != BSP_OK) {
return RES_ERROR;
}
}
memcpy(buff, s_cache_buf + offset, batch * 512);
buff += batch * 512;
sector += batch;
count -= (UINT)batch;
}
return RES_OK;
}
DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) {
if (pdrv != 0 || !buff) {
return RES_PARERR;
}
if (!s_initialized && disk_initialize(pdrv) != 0) {
return RES_NOTRDY;
}
while (count > 0) {
dhara_sector_t lpn = (dhara_sector_t)(sector / FTL_SECTORS_PER_PAGE);
uint32_t offset = (uint32_t)(sector % FTL_SECTORS_PER_PAGE) * 512;
uint32_t batch = FTL_SECTORS_PER_PAGE - (sector % FTL_SECTORS_PER_PAGE);
if (batch > count) {
batch = count;
}
if (lpn != s_cached_lpn) {
if (ftl_flush_cache() != BSP_OK) {
return RES_ERROR;
}
if (ftl_read_page(lpn) != BSP_OK) {
return RES_ERROR;
}
}
memcpy(s_cache_buf + offset, buff, batch * 512);
s_cache_dirty = 1;
if (offset == 0 && batch == FTL_SECTORS_PER_PAGE) {
if (ftl_flush_cache() != BSP_OK) {
return RES_ERROR;
}
}
buff += batch * 512;
sector += batch;
count -= (UINT)batch;
}
return RES_OK;
}
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) {
if (pdrv != 0) {
return RES_PARERR;
}
if (!s_initialized) {
return RES_NOTRDY;
}
switch (cmd) {
case CTRL_SYNC:
if (ftl_flush_cache() != BSP_OK) {
return RES_ERROR;
}
{
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;
}
}
return RES_OK;
case GET_SECTOR_COUNT: {
LBA_t *p_sectors = (LBA_t *)buff;
dhara_sector_t pages = dhara_map_capacity(&s_map);
*p_sectors = pages * FTL_SECTORS_PER_PAGE;
return RES_OK;
}
case GET_SECTOR_SIZE:
*(WORD *)buff = 512;
return RES_OK;
case GET_BLOCK_SIZE:
*(DWORD *)buff = 1;
return RES_OK;
default:
return RES_PARERR;
}
}
/* ======================== 公共函数定义 ======================== */
int nand_ftl_format(void) {
dhara_error_t err;
if (!s_initialized) {
if (disk_initialize(0) != 0) {
return GD5F_ERROR;
}
}
/* 处置开始:清掉历史失败快照,此后新失败重新记账
* (否则一次旧失败会永久污染"本次格式化到底成没成"的判定) */
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) {
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: %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;
}
DBG_INFO("NAND FTL formatted, capacity: %lu pages", dhara_map_capacity(&s_map));
return GD5F_OK;
}