Files
STM32F4-Base/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c
T
wandering1 04997e48aa refactor(编号): test/host 与 App/Drivers 注释去测试层级号(73 站点 / 64 文件)
同族第二道:代码注释面。41 个 run_unit_test.py 文件头与引用行、common harness 与两份
README、4 个 *_test.c 文件头、App/** 14 处(含 BOM 文件 App/cfg/cfg_file.c)、
GD5F nand_ftl.c 1 处,共 73 站点 / 64 文件;措辞取「主机侧纯逻辑单元测试运行器
(纯标准库)」避免同行「主机侧」重复。

零行为改动(机器证据,不是口头):
- 17 个 .c/.h 剥掉注释后与 HEAD **逐字节相同**;
- 42 个 .py 走 ast.dump 比较、仅归一 docstring(真实字符串字面量参与比较,故旗标或
  输出文本被动过必然报红)→ 全部相等;behaviour_changed=0。
- py_compile 42/42;抽跑 watchdog 47/47、freq 46/46(动手前先取基线,改后同数);
  CR 字节 0、EF BF BD 0、BOM 与 HEAD 逐一一致、check_bom_drift rc=0。

刻意未动:6 处字符串字面量(FTP 应答 "215 UNIX Type: L8"、lftpd_test.c SYST 断言、
console_log_test.c 的 "L1\r\n"、phasor_test.c 的 "L2-in ring" 文案与 printf 标题)——
改了就是改测试输出/行为;UI 设计令牌层 L0(ui_tokens.h、ui_content_page.c)与
test/host/README.md 的算法层号是**另外的族**。

两条 SyntaxWarning(ftp_cfg / net_cfg 文件头 \S 转义)**HEAD 同样存在**,已对 HEAD 副本
复现证明非本次引入,未顺手改。
2026-10-28 17:52:55 +08:00

654 lines
26 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_* 而忘改字面量会**静默**
* 把守卫算大 = 越界守卫失效,现改为直接编译错
*/
/* 头文件包含区 */
#include <string.h>
#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;
/* ======================== 私有函数声明 ======================== */
static bsp_err_t ftl_flush_cache(void);
static bsp_err_t ftl_read_page(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;
}
}
/* ======================== 页面缓存管理 ======================== */
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);
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);
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_ERASE_FAIL || ret != GD5F_OK) {
dhara_nand_mark_bad(n, b);
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) {
dhara_nand_mark_bad(n, p >> n->log2_ppb);
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);
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;
{
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));
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);
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;
}
}
if (ftl_flush_cache() != BSP_OK) {
return GD5F_ERROR;
}
dhara_map_clear(&s_map);
if (dhara_map_sync(&s_map, &err) < 0) {
DBG_ERROR("Map sync after clear failed: %d", err);
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;
}