78248a1cdf
前一笔立契约与门禁(`Drivers/BSP/bsp_err.h` + `bsp-err` 钩子 + RS485 试点);这一笔按用户
指定顺序 C3 → C1 → C2 把三个模块真正收编,并让存储失败第一次变得可读、可分类。
**C3 · SD2506 整模块(11 个公共 API + 6 个内部 helper)**:三枚私有宏 `SD2506_OK`/
`SD2506_ERROR`/`SD2506_I2C_ERROR` **删除而不是留别名**——留别名等于新代码还能用旧形状,
把迁移洗掉;编译器替我们找全所有调用点。新增 `sd2506_err_from_hal()`,**把 I2C 超时从
"和没人应答共用一个 −2"里拆出来归成 `BSP_ERR_TIMEOUT`(可重试)**——SD2506 是启动链第一站、
I2C1 上电时序敏感,这是本波唯一真正的语义改善,不是改名。`sd2506_init` 的 charge 寄存器
**回读校验不符**原返参数类错误,改归 `BSP_ERR_IO`(那不是调用方的错)。
**C1 · 同形两义**:`GD5F_BUSY_TIMEOUT` → `GD5F_WAIT_EXPIRED`(**值 −2 不变**,纯消"名字叫
TIMEOUT 其实是返回码")。**GD5F 私有码集整体收编刻意没做**:169 处引用里 48 处在板级套件、
还要动 Trap 记录用词,属另一波带真机+套件回归的工作,不夹带。TPAFE5160 收编 23 处:
`BUSY_TIMEOUT → BSP_ERR_TIMEOUT`、`ERROR → BSP_ERR_PARAM`(**同值 −1,行为零变化**)、
`FRSTDATA_ERR → BSP_ERR_NO_DEVICE`(E-44 的本义就是"芯片没驱动 DB 总线",归到器件无响应比
私有 −3 有用,且避开与 `BSP_ERR_TIMEOUT` 撞值)。实测 `tpafe5160.o`/`gd5f2gq5ue.o` 的 Code
增量为 **0**——改名不产生一条指令。
⚠ **`tpafe5160_snapshot()` 刻意不套成败码**:它是 1=有新帧 / 0=无新帧 / −1=参数错 的三态
**状态**返回,0 不是错误而是"这一拍没有新数据",强行收编会把慢消费者的正常路径变成失败。
顺带订正我上一版在 `bsp_err.h` 的断言——"本层只有 `net_*` 把数量与错误挤同一返回值"是错的,
`snapshot`/`adc_input_poll` 就是第二族;两族现都已写明并各注保留理由。
**C2 · FTL 坍缩点分类**:`ftl_flush_cache`/`ftl_read_page` 不再把 dhara 失败压成
`GD5F_ERROR`,
按**处置差异**分三类,并在最早同时知道 LPN 与原始码的位置就地打印:`ECC`/`TOO_BAD` →
`BSP_ERR_CRC`(页级,重试或换页,不代表卷坏);`JOURNAL_FULL`/`MAP_FULL` →
`BSP_ERR_NO_SPACE`(介质健康但块池/日志见底,此时重格式化只是把问题推回去);
`BAD_BLOCK`/`RECOVER`/`CORRUPT_MAP`/越界 → `BSP_ERR_IO`(完整性故障,保卷 + 上报,不许自愈
重格式化)。这与 **E-31** 是同一条纪律的两端。可读名用自建短表:**本仓 dhara 没有编
`error.c`,用 `dhara_strerror()` 会直接链接失败**。`disk_read`/`disk_write` 仍回
`RES_ERROR`
(FatFs 契约);要让 defaultTask 卷健康探针按类分支还差跨调用存活的快照 → C4 待还。
**两次靠核实拦住的自我错误**:① 我一度认定 `DBG_ERROR("... err=%d", err)` 是把结构体塞进
varargs 的格式 bug——核 `Lib/dhara/error.h` 后确认 `dhara_error_t` 是 `enum`(标量),那行日志
本来合法,改了反而是引入错误;② `bsp-err` 一次 `replace_all` 越界把 8 处 **GD5F 码空间**的
判据改成 `!= BSP_OK`(数值同为 0、无行为变化),但那正是"把两个码空间混起来",已逐一还原。
`cppcheck` 亦抓到 `cls` 声明作用域过宽,已收进失败分支。
**验证**:`@build` 0 错 0 警告(`Code` 213,648、RO 18,860、`ER_IROM1` 232,508 B /
22.2%、
`ROM 合计` 233,696;**RAM 三项零增量**);`@test` PASS(692 ms,烧入戳 `Oct 23 01:35:32`)
且逐条读到被改模块的锚点:`SD2506 init: OK (ret=0)` 与 ID/时间/温度/电池四条读回正常、
`ADC init: OK`、`NAND init: OK`、`Map resumed: 14094/97504`、`adc=` 帧计数持续递增(证明
snapshot 三态未被改坏)、`[ERR]`/Fault/FAIL 0 条。主机单测:系统时钟 100/100、dhara NAND
移植层 149/149(两者都因本次改动需补 `Drivers/BSP` include 路径,补后在钩子内实跑 Passed)。
`pre-commit run` 全量 RC=0(20 个实跑钩子 Passed);`bsp-err` 基线 **19 → 17**(还掉两个
`*_BUSY_TIMEOUT = -2`)、`--prune` 干净;`doc-links --all --sections-strict` 147 篇
0 问题;
`id-check --strict`、`layer-deps`、`mem-numbers`、`mem-budget`、`ui-golden` 全过;33
个文件
字节卫生 0 异常(无 CR / 无 U+FFFD / 无行尾空格 / 均有尾换行)。
**口径与数字联动**:新增 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.4(三类码与处置
理由);`pre-commit使用说明.md` §2.27 与 `AGENTS.md` 基线计数 19 → 17;TPAFE README、
GD5F 测试指南与 Trap 记录均**保留旧名留痕**以便对旧日志;配图桶按 .map 对象级增量重算
(`dhara/FTL` 10,931 → 11,222,+291 全部落在 `nand_ftl.o`;应用代码/UI +36 落在
`app_main.o`;其余散量入"其他"7,557;桶和 = Code+RO 断言通过、文本溢出 0);
`ram_baseline --update/--write`、`ram_report_20261023.md` 快照、报告 §1.3/§5.1/§5.4
与
部署文档 §3.3 数字全部同步;报告 §0.4/§9 的 S-5 状态改为 🟡 主体已落地。
652 lines
26 KiB
C
652 lines
26 KiB
C
/*
|
||
* 模块名称:NAND FTL 适配层(dhara + FatFS diskio 胶水)
|
||
* 模块功能:实现 dhara NAND HAL 和 FatFS 磁盘 I/O,连接 GD5F2GQ5UE 底层驱动
|
||
* 适用平台:STM32F407ZGT6
|
||
* 作者:王建锋
|
||
* 创建日期:2026-07-20
|
||
* 修改记录:
|
||
* 2026-07-20 王建锋 创建初始版本
|
||
* 2026-09-21 E-31 resume 失败按错误分类:仅真空白判新卷,其余失败
|
||
* 返回 mount 层(禁止静默清 map 触发整卷重格式化)
|
||
* 2026-10-02 王建锋 resume 成功后增打 journal head/tail/epoch:复盘
|
||
* "复位后所有写入丢失"时 sectors used 不足以判定
|
||
* journal 回退(覆盖写不改 count),head/tail/epoch 是
|
||
* 唯一能暴露 resume 锚错 checkpoint 的证据
|
||
* 2026-10-15 王建锋 N-21(报告 N-12/N-13):① is_free 改**整页扫描**——
|
||
* 旧实现只看页首 64 B,会把"页首 0xFF、后段有数据"的
|
||
* 已编程页误判空闲,dhara 遂往已编程页再编程 = 数据丢失
|
||
* (页首有数据的绝大多数页走快路径,成本与旧实现同价);
|
||
* ② HAL 六个入口全部加几何上界守卫——越界页/块号(含
|
||
* DHARA_PAGE_NONE)旧会被地址位截断成真实页 → 静默读写
|
||
* 错页,现报 DHARA_E_CORRUPT_MAP(不能用 BAD_BLOCK:会
|
||
* 误标无辜好块;不能用 RECOVER:会触发回收);
|
||
* ③ 补主机侧移植层单测 test/host/nand_port/(L0.5);
|
||
* ④ 几何字面量收成 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 的 N-26 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; }
|
||
|
||
/*
|
||
* N-21(2026-10-15,报告 N-13):移植层地址守卫
|
||
*
|
||
* 旧实现把 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 启动回收,
|
||
* 其余错误一律原样上抛(E-31 教训:单次瞬态毛刺不得销毁整卷数据)。
|
||
* is_free / is_bad 没有错误通道 → 一律保守取值(判非空 / 判坏),与 E-42 同族
|
||
* 口径一致:宁可少用一个块,不可把不可信的内容当有效。
|
||
*/
|
||
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,报告 S-5)
|
||
* 入口参数: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 → 介质还健康,是**块池/日志预算见底**,此时重格式化只是
|
||
* 把问题推回去(E-31 的教训正是"别把非空白错误当新卷清 map"),该走容量恢复流程;
|
||
* · BAD_BLOCK/CORRUPT_MAP/RECOVER → 映射或介质完整性故障,该保卷 + 上报,不许自愈重格式化。
|
||
* 本函数先把这三类**打出来**(就地日志,最早且唯一同时知道 LPN 与原始码的位置);
|
||
* 要让 defaultTask 的卷健康探针按类分支,还需要一个跨调用存活的快照(S-5 的 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;
|
||
}
|
||
|
||
/*
|
||
* N-21(2026-10-15,报告 N-12):整页扫描
|
||
*
|
||
* 旧实现只看页首 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(页缓冲三块不可合并的评估见报告 R-3)。
|
||
*/
|
||
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) {
|
||
/* E-42 同族(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;
|
||
}
|
||
|
||
/* E-42 同族(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;
|
||
}
|
||
|
||
/*
|
||
* N-21(2026-10-15,报告 N-13):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...");
|
||
|
||
/* N-26 启动期地址自检(2026-10-12):本文件三个页缓冲是 SPI1 DMA 路径上
|
||
* 唯一的**静态**缓冲(dhara_map_read/write → dhara_nand_read/prog/copy,
|
||
* 2048 B > GD5F_DMA_THRESHOLD;s_page_buf 现仅装 dhara meta 132 B 走轮询,
|
||
* 但它同为页尺寸且交给 dhara 内部使用,一并守住防将来越窗)。
|
||
* DMA1/DMA2 不可达 CCM——若 scatter 改动/段放置把它们挪进 CCM,DMA 会
|
||
* **静默不落地**(数据错而传输"成功")。缓冲地址链接期才确定,armcc
|
||
* 无法编译期断言(见 gd5f2gq5ue.h 的窗口常量注释),故在 init 期自检:
|
||
* 任一缓冲越出 DMA 白名单窗口 → 上报并拒绝挂载(存储降级、不挂死,
|
||
* 与 E-31 的"保留卷、上报自检"形态一致)。每次传输的运行期守卫
|
||
* (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 {
|
||
/* E-31(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;
|
||
}
|