969 lines
32 KiB
C
969 lines
32 KiB
C
/*
|
||
* 模块名称:NAND FTL 适配层(dhara + FatFS diskio 胶水)
|
||
* 模块功能:实现 dhara NAND HAL 和 FatFS 磁盘 I/O,连接 GD5F2GQ5UE 底层驱动
|
||
* 适用平台:STM32F407ZGT6
|
||
* 作者:王建锋
|
||
* 创建日期:2026-07-20
|
||
* 修改记录:
|
||
* 2026-07-20 王建锋 创建初始版本
|
||
*/
|
||
|
||
/* 头文件包含区 */
|
||
#include <string.h>
|
||
#include "gd5f2gq5ue.h"
|
||
#include "ff.h"
|
||
#include "diskio.h"
|
||
#include "nand.h"
|
||
#include "map.h"
|
||
|
||
/* FTL 分区起始地址(块 0,偏移 0,占用全部 256MB) */
|
||
#define FTL_FATFS_OFFSET 0U
|
||
|
||
/* 调试输出配置 */
|
||
#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 逻辑地址到物理地址转换 */
|
||
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 核心数据结构 */
|
||
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;
|
||
|
||
/* resume 搜索探针日志窗口(诊断:逐探测记录判定输入) */
|
||
static uint8_t s_probe_log;
|
||
/* L0 卷健康探针窗口(诊断:记录 FAT 读的 LPN 绑定 → 物理页路径) */
|
||
static uint8_t s_tree_probe_log;
|
||
|
||
/*
|
||
* 函数功能:打开/关闭 L0 卷健康探针日志窗口
|
||
* 入口参数:on - 1 打开,0 关闭
|
||
* 返回值:无
|
||
* 限定条件:FTL 已初始化
|
||
* 函数说明:窗口内的 dhara_nand_read/is_free/is_bad 调用逐条记录
|
||
* 物理页(= journal LPN),用于判定 map 绑定指向
|
||
*/
|
||
void nand_ftl_set_tree_probe(uint8_t on) {
|
||
s_tree_probe_log = on;
|
||
}
|
||
|
||
/* ======================== 私有函数声明 ======================== */
|
||
|
||
static int ftl_flush_cache(void);
|
||
static int ftl_read_page(dhara_sector_t lpn);
|
||
static void ftl_scan_checks_raw(void);
|
||
|
||
/* SPI 原语从 gd5f2gq5ue.h 获取 */
|
||
|
||
/* ======================== 页面缓存管理 ======================== */
|
||
|
||
static int ftl_flush_cache(void) {
|
||
if (!s_cache_dirty) {
|
||
return GD5F_OK;
|
||
}
|
||
|
||
dhara_error_t err;
|
||
if (dhara_map_write(&s_map, s_cached_lpn, s_cache_buf, &err) < 0) {
|
||
DBG_ERROR("Cache flush failed: LPN=%lu err=%d", s_cached_lpn, err);
|
||
return GD5F_ERROR;
|
||
}
|
||
|
||
s_cache_dirty = 0;
|
||
return GD5F_OK;
|
||
}
|
||
|
||
static int ftl_read_page(dhara_sector_t lpn) {
|
||
dhara_error_t err;
|
||
|
||
if (dhara_map_read(&s_map, lpn, s_cache_buf, &err) < 0) {
|
||
DBG_ERROR("Page read failed: LPN=%lu err=%d", lpn, err);
|
||
return GD5F_ERROR;
|
||
}
|
||
|
||
s_cached_lpn = lpn;
|
||
s_cache_dirty = 0;
|
||
if (s_tree_probe_log) {
|
||
/* 内容指纹:前 8 字节(BPB=33 52 / FAT32=F8 FF.. / 擦除=FF FF..)
|
||
* 半新半旧混合页在此直接现形 */
|
||
DBG_INFO("ftl t %lu read LPN=%lu: %02X %02X %02X %02X %02X %02X %02X %02X",
|
||
(unsigned long)HAL_GetTick(), (unsigned long)lpn,
|
||
(unsigned int)s_cache_buf[0], (unsigned int)s_cache_buf[1],
|
||
(unsigned int)s_cache_buf[2], (unsigned int)s_cache_buf[3],
|
||
(unsigned int)s_cache_buf[4], (unsigned int)s_cache_buf[5],
|
||
(unsigned int)s_cache_buf[6], (unsigned int)s_cache_buf[7]);
|
||
}
|
||
return GD5F_OK;
|
||
}
|
||
|
||
/* ======================== dhara NAND HAL ======================== */
|
||
|
||
int dhara_nand_is_bad(const struct dhara_nand *n, dhara_block_t b) {
|
||
(void)n;
|
||
const int bad = gd5f2gq5ue_is_block_bad(ftl_phy_block(b));
|
||
if (s_probe_log || s_tree_probe_log) {
|
||
DBG_INFO("probe blk %lu: bbt_bad=%u", (unsigned long)b,
|
||
(unsigned int)bad);
|
||
}
|
||
return bad;
|
||
}
|
||
|
||
void dhara_nand_mark_bad(const struct dhara_nand *n, dhara_block_t b) {
|
||
(void)n;
|
||
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) {
|
||
(void)n;
|
||
int 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 = ftl_phy_page(p);
|
||
|
||
if (s_probe_log || s_tree_probe_log) {
|
||
DBG_INFO("probe t %lu prog pg %lu blk %lu g%lu",
|
||
(unsigned long)HAL_GetTick(), (unsigned long)phy_pg,
|
||
(unsigned long)(p >> 6), (unsigned long)((p >> 4) & 3u));
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/* 编程回读校验(2026-09-20 台架实证):供电低于标称(3.1V)时
|
||
* program 可"状态成功"但 cell 未可靠 latch(ECC 不可纠或数据错位)——
|
||
* 状态寄存器发现不了,下次复位 journal resume 因 ECC 门拒绝这些
|
||
* checkpoint 页 → 新写入整体静默丢失(重格式化循环的真根因)。
|
||
* 回读比对+ECC 检查,失败则标记坏块由 dhara 迁移,宁可丢块不丢卷。 */
|
||
if (gd5f_page_read(phy_pg) != GD5F_OK) {
|
||
dhara_nand_mark_bad(n, p >> n->log2_ppb);
|
||
dhara_set_error(err, DHARA_E_ECC);
|
||
DBG_ERROR("program verify: readback fail, blk %lu",
|
||
(unsigned long)(p >> n->log2_ppb));
|
||
return -1;
|
||
}
|
||
|
||
if (gd5f_check_ecc() != GD5F_OK) {
|
||
dhara_nand_mark_bad(n, p >> n->log2_ppb);
|
||
dhara_set_error(err, DHARA_E_ECC);
|
||
DBG_ERROR("program verify: ecc uncorrectable, blk %lu",
|
||
(unsigned long)(p >> n->log2_ppb));
|
||
return -1;
|
||
}
|
||
|
||
gd5f_read_from_cache(0, s_copy_buf, FTL_PAGE_SIZE);
|
||
if (memcmp(s_copy_buf, data, FTL_PAGE_SIZE) != 0) {
|
||
dhara_nand_mark_bad(n, p >> n->log2_ppb);
|
||
dhara_set_error(err, DHARA_E_BAD_BLOCK);
|
||
DBG_ERROR("program verify: data mismatch, blk %lu",
|
||
(unsigned long)(p >> n->log2_ppb));
|
||
return -1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
int dhara_nand_is_free(const struct dhara_nand *n, dhara_page_t p) {
|
||
(void)n;
|
||
uint32_t phy_pg = ftl_phy_page(p);
|
||
int fre;
|
||
|
||
if (gd5f_page_read(phy_pg) != GD5F_OK) {
|
||
fre = 0;
|
||
} else {
|
||
uint8_t buf[64];
|
||
uint32_t i;
|
||
|
||
gd5f_read_from_cache(0, buf, sizeof(buf));
|
||
fre = 1;
|
||
for (i = 0; i < sizeof(buf); i++) {
|
||
if (buf[i] != 0xFF) {
|
||
fre = 0;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (s_probe_log || s_tree_probe_log) {
|
||
DBG_INFO("probe t %lu isfree pg %lu = %u",
|
||
(unsigned long)HAL_GetTick(), (unsigned long)phy_pg,
|
||
(unsigned int)fre);
|
||
}
|
||
return fre;
|
||
}
|
||
|
||
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) {
|
||
(void)n;
|
||
uint32_t phy_pg = ftl_phy_page(p);
|
||
int rret;
|
||
int eret;
|
||
|
||
if ((rret = gd5f_page_read(phy_pg)) != GD5F_OK) {
|
||
if (s_probe_log || s_tree_probe_log) {
|
||
DBG_INFO("probe pg %lu blk %lu: page_read=%d",
|
||
(unsigned long)phy_pg, (unsigned long)(p >> 6),
|
||
rret);
|
||
}
|
||
dhara_set_error(err, DHARA_E_ECC);
|
||
return -1;
|
||
}
|
||
|
||
if ((eret = gd5f_check_ecc()) != GD5F_OK) {
|
||
if (s_probe_log || s_tree_probe_log) {
|
||
DBG_INFO("probe pg %lu blk %lu: ecc=%d",
|
||
(unsigned long)phy_pg, (unsigned long)(p >> 6),
|
||
eret);
|
||
}
|
||
dhara_set_error(err, DHARA_E_ECC);
|
||
return -1;
|
||
}
|
||
|
||
gd5f_read_from_cache((uint16_t)offset, data, length);
|
||
|
||
if ((s_probe_log || s_tree_probe_log) &&
|
||
offset == 0 && length == FTL_PAGE_SIZE) {
|
||
/* 组末页(组内偏移 15:页号低 4 位 = 15)= checkpoint 页:
|
||
* 记录 header 前 4 字节 + meta 区 4 字节(偏移 944 = 槽 7,
|
||
* 树根 meta)+ 时间戳。header 新/meta 旧 = 半新半旧混合页
|
||
* (低 VDD 就绪信号提前竞态的直接证据) */
|
||
if ((p & 15u) == 15u) {
|
||
const uint8_t *m = data;
|
||
DBG_INFO("probe t %lu pg %lu blk %lu g%lu: %02X %02X %02X %02X"
|
||
" meta %02X %02X %02X %02X",
|
||
(unsigned long)HAL_GetTick(),
|
||
(unsigned long)phy_pg, (unsigned long)(p >> 6),
|
||
(unsigned long)((p >> 4) & 3u),
|
||
(unsigned int)m[0], (unsigned int)m[1],
|
||
(unsigned int)m[2], (unsigned int)m[3],
|
||
(unsigned int)m[944], (unsigned int)m[945],
|
||
(unsigned int)m[946], (unsigned int)m[947]);
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
int dhara_nand_copy(const struct dhara_nand *n, dhara_page_t src, dhara_page_t dst, dhara_error_t *err) {
|
||
(void)n;
|
||
|
||
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...");
|
||
|
||
s_nand.log2_page_size = 11;
|
||
s_nand.log2_ppb = 6;
|
||
/* dhara 可见块数 = 总块数 - 保留块池(BBT 存储区不交给 dhara,避免被擦写) */
|
||
s_nand.num_blocks = gd5f_get_usable_blocks();
|
||
|
||
dhara_map_init(&s_map, &s_nand, s_page_buf, 4);
|
||
|
||
/* 诊断:resume 前的裸 NAND checkpoint 扫描(复位后地面真值,
|
||
* 不经 FTL 缓存)——判定上一开机的 checkpoint 页是否真正写入芯片 */
|
||
DBG_INFO("Pre-resume raw check scan start: blocks=%lu",
|
||
(unsigned long)s_nand.num_blocks);
|
||
ftl_scan_checks_raw();
|
||
|
||
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;
|
||
s_probe_log = 1;
|
||
if (dhara_map_resume(&s_map, &err) < 0) {
|
||
DBG_INFO("No valid map found, creating fresh (err=%d)", err);
|
||
dhara_map_clear(&s_map);
|
||
} else {
|
||
DBG_INFO("Map resumed: %lu/%lu sectors used",
|
||
dhara_map_size(&s_map), dhara_map_capacity(&s_map));
|
||
}
|
||
s_probe_log = 0;
|
||
}
|
||
|
||
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() != GD5F_OK) {
|
||
return RES_ERROR;
|
||
}
|
||
if (ftl_read_page(lpn) != GD5F_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() != GD5F_OK) {
|
||
return RES_ERROR;
|
||
}
|
||
if (ftl_read_page(lpn) != GD5F_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() != GD5F_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() != GD5F_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() != GD5F_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;
|
||
}
|
||
|
||
/* ======================== 诊断(跨复位日志持久性分析) ======================== */
|
||
|
||
/* 打印当前 journal/map 状态(epoch/head/tail/root/容量) */
|
||
void nand_ftl_log_state(const char *tag)
|
||
{
|
||
const struct dhara_journal *j = &s_map.journal;
|
||
|
||
if (!s_initialized) {
|
||
return;
|
||
}
|
||
|
||
DBG_INFO("FTL state %s: epoch=%u head=%lu tail=%lu root=%lu jsize=%lu map=%lu",
|
||
tag, (unsigned int)j->epoch,
|
||
(unsigned long)j->head, (unsigned long)j->tail,
|
||
(unsigned long)j->root,
|
||
(unsigned long)dhara_journal_size(j),
|
||
(unsigned long)dhara_map_size(&s_map));
|
||
}
|
||
|
||
/* 打印某块 checkpoint 页的 16 字节 header(magic/epoch/tail/bb),
|
||
* 用于区分"未落盘(全FF)"/"编程失败(花)"/"完整" 三种状态 */
|
||
static void ftl_dump_checkheader(uint32_t blk)
|
||
{
|
||
uint8_t hdr[16];
|
||
|
||
const uint32_t pg = ftl_phy_page((blk << s_nand.log2_ppb) |
|
||
((1u << s_map.journal.log2_ppc) - 1u));
|
||
if (gd5f_page_read(pg) != GD5F_OK) {
|
||
DBG_INFO("check hdr blk %lu: read fail", (unsigned long)blk);
|
||
return;
|
||
}
|
||
gd5f_read_from_cache(0, hdr, sizeof(hdr));
|
||
DBG_INFO("check hdr blk %lu: %02X %02X %02X %02X %02X %02X %02X %02X "
|
||
"%02X %02X %02X %02X %02X %02X %02X %02X",
|
||
(unsigned long)blk,
|
||
(unsigned int)hdr[0], (unsigned int)hdr[1], (unsigned int)hdr[2],
|
||
(unsigned int)hdr[3], (unsigned int)hdr[4], (unsigned int)hdr[5],
|
||
(unsigned int)hdr[6], (unsigned int)hdr[7],
|
||
(unsigned int)hdr[8], (unsigned int)hdr[9], (unsigned int)hdr[10],
|
||
(unsigned int)hdr[11], (unsigned int)hdr[12], (unsigned int)hdr[13],
|
||
(unsigned int)hdr[14], (unsigned int)hdr[15]);
|
||
}
|
||
|
||
/* 全扫每块首组 checkpoint 页(magic+epoch),打印连续区段分布,
|
||
* 用于识别 journal 环上的 epoch 分界与空洞。
|
||
* 裸读(gd5f 页读),不受 FTL 缓存影响 = NAND 真实内容。 */
|
||
static void ftl_scan_checks_raw(void)
|
||
{
|
||
uint32_t i;
|
||
uint8_t buf[8];
|
||
uint32_t total = 0;
|
||
uint32_t run_start = 0;
|
||
uint32_t run_prev = 0;
|
||
uint32_t run_len = 0;
|
||
uint32_t run_ecc_fail = 0;
|
||
uint32_t run_ecc_first = 0;
|
||
uint32_t run_ecc_last = 0;
|
||
uint32_t run_bbt_bad = 0;
|
||
uint32_t run_bbt_first = 0;
|
||
uint32_t run_bbt_last = 0;
|
||
uint8_t run_epoch = 0;
|
||
uint8_t run_open = 0;
|
||
|
||
for (i = 0; i < s_nand.num_blocks; i++) {
|
||
const uint32_t pg = ftl_phy_page((i << s_nand.log2_ppb) |
|
||
((1u << s_map.journal.log2_ppc) - 1u));
|
||
|
||
if (gd5f_page_read(pg) != GD5F_OK) {
|
||
continue;
|
||
}
|
||
/* ECC 状态在 0Bh 之前检查(与 dhara_nand_read 的读取顺序一致,
|
||
* 排除 read-from-cache 对状态的干扰) */
|
||
int ecc = gd5f_check_ecc();
|
||
gd5f_read_from_cache(0, buf, sizeof(buf));
|
||
if (buf[0] == 'D' && buf[1] == 'h' && buf[2] == 'a') {
|
||
const uint8_t ep = buf[3];
|
||
|
||
total++;
|
||
if (ecc != GD5F_OK) {
|
||
run_ecc_fail++;
|
||
if (!run_ecc_fail) {
|
||
run_ecc_first = i;
|
||
}
|
||
run_ecc_last = i;
|
||
}
|
||
if (gd5f2gq5ue_is_block_bad(ftl_phy_block(i))) {
|
||
run_bbt_bad++;
|
||
if (!run_bbt_bad) {
|
||
run_bbt_first = i;
|
||
}
|
||
run_bbt_last = i;
|
||
}
|
||
if (!run_open || (i != run_prev + 1) || (ep != run_epoch)) {
|
||
if (run_open) {
|
||
DBG_INFO("check run: blk %lu..%lu epoch=%u (%lu blocks)",
|
||
(unsigned long)run_start, (unsigned long)run_prev,
|
||
(unsigned int)run_epoch, (unsigned long)run_len);
|
||
if (run_ecc_fail) {
|
||
DBG_INFO(" run ecc-fail: %lu blocks (blk %lu..%lu)",
|
||
(unsigned long)run_ecc_fail,
|
||
(unsigned long)run_ecc_first,
|
||
(unsigned long)run_ecc_last);
|
||
}
|
||
if (run_bbt_bad) {
|
||
DBG_INFO(" run bbt-bad: %lu blocks (blk %lu..%lu)",
|
||
(unsigned long)run_bbt_bad,
|
||
(unsigned long)run_bbt_first,
|
||
(unsigned long)run_bbt_last);
|
||
}
|
||
ftl_dump_checkheader(run_start);
|
||
}
|
||
run_start = i;
|
||
run_epoch = ep;
|
||
run_len = 0;
|
||
run_ecc_fail = 0;
|
||
run_bbt_bad = 0;
|
||
run_open = 1;
|
||
}
|
||
run_prev = i;
|
||
run_len++;
|
||
}
|
||
}
|
||
if (run_open) {
|
||
DBG_INFO("check run: blk %lu..%lu epoch=%u (%lu blocks)",
|
||
(unsigned long)run_start, (unsigned long)run_prev,
|
||
(unsigned int)run_epoch, (unsigned long)run_len);
|
||
if (run_ecc_fail) {
|
||
DBG_INFO(" run ecc-fail: %lu blocks (blk %lu..%lu)",
|
||
(unsigned long)run_ecc_fail,
|
||
(unsigned long)run_ecc_first,
|
||
(unsigned long)run_ecc_last);
|
||
}
|
||
if (run_bbt_bad) {
|
||
DBG_INFO(" run bbt-bad: %lu blocks (blk %lu..%lu)",
|
||
(unsigned long)run_bbt_bad,
|
||
(unsigned long)run_bbt_first,
|
||
(unsigned long)run_bbt_last);
|
||
}
|
||
ftl_dump_checkheader(run_start);
|
||
}
|
||
DBG_INFO("check scan done: %lu checkpoint blocks", (unsigned long)total);
|
||
}
|
||
|
||
void nand_ftl_scan_checks(void)
|
||
{
|
||
if (!s_initialized) {
|
||
return;
|
||
}
|
||
|
||
DBG_INFO("check scan start: blocks=%lu", (unsigned long)s_nand.num_blocks);
|
||
ftl_scan_checks_raw();
|
||
}
|
||
|
||
/* ============ 裸片读写擦自检(2026-09-20 诊断:驱动层 / FTL 层 / 电源定界) ============ */
|
||
|
||
/*
|
||
* 测试块选择:2040/2041(芯片顶部区,远离 journal 环头,数千次开机内
|
||
* 不会被环覆盖;不在 BBT 保留池 2044..2047)。使用块内页 1..4,
|
||
* 避开页 0(出厂坏块标记位 page0 spare[0],防 BBT 扫描误判)。
|
||
*/
|
||
#define RAW_T_BLK_A 2040u /* 数据块:先回读上一boot图案(持久性),再擦除写新图案 */
|
||
#define RAW_T_BLK_B 2041u /* 纯擦块:每次擦除后校验全 FF */
|
||
#define RAW_T_PAGE0 1u /* 块内起始使用页(跳过页 0) */
|
||
#define RAW_T_NPG 4u /* 测试页数 */
|
||
|
||
/*
|
||
* 函数功能:计算测试页第 i 字节的期望值(seq+page 的确定性函数)
|
||
* 入口参数:seq - 启动序号 uint32_t
|
||
* page - 逻辑页号 0..3 uint32_t
|
||
* i - 页内偏移 uint32_t
|
||
* 返回值:期望字节值 uint8_t
|
||
* 限定条件:无
|
||
* 函数说明:页头 10 字节 = magic "RAW1" + seq(LE32) + page(LE16),
|
||
* 其余 2038 字节 = (seq*31 + page*7 + i) 的伪随机填充。
|
||
*/
|
||
static uint8_t raw_t_byte(uint32_t seq, uint32_t page, uint32_t i) {
|
||
switch (i) {
|
||
case 0u: return (uint8_t)'R';
|
||
case 1u: return (uint8_t)'A';
|
||
case 2u: return (uint8_t)'W';
|
||
case 3u: return (uint8_t)'1';
|
||
case 4u: return (uint8_t)(seq & 0xFFu);
|
||
case 5u: return (uint8_t)((seq >> 8) & 0xFFu);
|
||
case 6u: return (uint8_t)((seq >> 16) & 0xFFu);
|
||
case 7u: return (uint8_t)((seq >> 24) & 0xFFu);
|
||
case 8u: return (uint8_t)(page & 0xFFu);
|
||
case 9u: return (uint8_t)((page >> 8) & 0xFFu);
|
||
default: return (uint8_t)(seq * 31u + page * 7u + i);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:填充测试图案到缓冲区
|
||
* 入口参数:p_buf - 目标缓冲区 uint8_t* 长度 FTL_PAGE_SIZE
|
||
* seq - 启动序号 uint32_t
|
||
* page - 逻辑页号 0..3 uint32_t
|
||
* 返回值:无
|
||
* 限定条件:无
|
||
* 函数说明:图案内容见 raw_t_byte。
|
||
*/
|
||
static void raw_t_fill(uint8_t *p_buf, uint32_t seq, uint32_t page) {
|
||
uint32_t i;
|
||
|
||
for (i = 0; i < FTL_PAGE_SIZE; i++) {
|
||
p_buf[i] = raw_t_byte(seq, page, i);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:缓冲区与期望图案逐字节比对
|
||
* 入口参数:p_buf - 待比对缓冲 const uint8_t* 长度 FTL_PAGE_SIZE
|
||
* seq - 期望启动序号 uint32_t
|
||
* page - 期望逻辑页号 uint32_t
|
||
* p_off - 差异偏移输出 uint32_t*(仅失配时有效)
|
||
* 返回值:0 - 全部一致,-1 - 存在差异
|
||
* 限定条件:无
|
||
* 函数说明:期望内容见 raw_t_byte。
|
||
*/
|
||
static int raw_t_cmp(const uint8_t *p_buf, uint32_t seq, uint32_t page,
|
||
uint32_t *p_off) {
|
||
uint32_t i;
|
||
|
||
for (i = 0; i < FTL_PAGE_SIZE; i++) {
|
||
if (p_buf[i] != raw_t_byte(seq, page, i)) {
|
||
*p_off = i;
|
||
return -1;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:读一个物理整页到 s_copy_buf
|
||
* 入口参数:page_addr - 物理页地址 uint32_t
|
||
* 返回值:0 - 成功,其它 - 错误码
|
||
* 限定条件:ECC 已使能;s_copy_buf 空闲
|
||
* 函数说明:13h 装载(驱动内含二次发令+沉降兜底)→ 0Bh 整页流出。
|
||
*/
|
||
static int raw_t_read(uint32_t page_addr) {
|
||
int ret = gd5f_page_read(page_addr);
|
||
|
||
if (ret != GD5F_OK) {
|
||
return ret;
|
||
}
|
||
return gd5f_read_from_cache(0, s_copy_buf, FTL_PAGE_SIZE);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:裸 NAND 读 / 擦 / 写 / 掉电持久性自检(诊断用)
|
||
* 入口参数:无
|
||
* 返回值:无
|
||
* 限定条件:gd5f2gq5ue_init() 成功后、FTL 初始化前调用(内核前,
|
||
* 无并发;s_copy_buf 此时未被 FTL 占用,复用安全)
|
||
* 函数说明:每次开机顺序执行三步,三条日志合起来定界故障域:
|
||
* 1) persist — 块 A 回读"上一 boot"写入的 seq 图案(擦除前!)
|
||
* → 数据跨复位是否保得住;
|
||
* 2) erase — 擦块 A/B(返回码含 E_FAIL)+ 读回全 FF 校验
|
||
* → 擦除是否真实落盘;
|
||
* 3) write — 块 A 写新 seq 图案 + 立即回读逐字节比对(wv)
|
||
* → 编程当下是否落盘。
|
||
* 判读:wv 全过而 persist 丢失 ⇒ 编程未写入非易失单元(掉电丢);
|
||
* erase 返回 OK 而 ff 不满 ⇒ 擦除未落盘;wv 丢 ⇒ 驱动/命令通路问题。
|
||
*/
|
||
void nand_ftl_raw_persist_test(void) {
|
||
uint32_t k;
|
||
uint32_t pg;
|
||
uint32_t seq_prev = 0u;
|
||
uint8_t magic_ok = 0u;
|
||
uint32_t off = 0u;
|
||
uint32_t seq_new;
|
||
uint32_t wv = 0u;
|
||
int erase_a;
|
||
int erase_b;
|
||
uint32_t ff_a = 0u;
|
||
uint32_t ff_b = 0u;
|
||
|
||
/* ---- 1) 持久性检查:回读上一 boot 写入的图案(必须先于擦除) ---- */
|
||
pg = (RAW_T_BLK_A * GD5F_PAGES_PER_BLOCK) + RAW_T_PAGE0;
|
||
if (raw_t_read(pg) == GD5F_OK) {
|
||
if (s_copy_buf[0] == (uint8_t)'R' && s_copy_buf[1] == (uint8_t)'A' &&
|
||
s_copy_buf[2] == (uint8_t)'W' && s_copy_buf[3] == (uint8_t)'1') {
|
||
magic_ok = 1u;
|
||
seq_prev = (uint32_t)s_copy_buf[4] |
|
||
((uint32_t)s_copy_buf[5] << 8) |
|
||
((uint32_t)s_copy_buf[6] << 16) |
|
||
((uint32_t)s_copy_buf[7] << 24);
|
||
}
|
||
}
|
||
|
||
if (magic_ok != 0u) {
|
||
uint32_t persist_ok = 0u;
|
||
|
||
for (k = 0u; k < RAW_T_NPG; k++) {
|
||
pg = (RAW_T_BLK_A * GD5F_PAGES_PER_BLOCK) + RAW_T_PAGE0 + k;
|
||
if (raw_t_read(pg) != GD5F_OK) {
|
||
break;
|
||
}
|
||
if (raw_t_cmp(s_copy_buf, seq_prev, k, &off) != 0) {
|
||
DBG_ERROR("[RAW] persist corrupt pg %lu off %lu exp %02X got %02X",
|
||
(unsigned long)(RAW_T_PAGE0 + k),
|
||
(unsigned long)off,
|
||
(unsigned int)raw_t_byte(seq_prev, k, off),
|
||
(unsigned int)s_copy_buf[off]);
|
||
break;
|
||
}
|
||
persist_ok++;
|
||
}
|
||
DBG_INFO("[RAW] persist: seq=%lu ok=%u/%lu (prev-boot data after reset)",
|
||
(unsigned long)seq_prev, (unsigned int)persist_ok,
|
||
(unsigned long)RAW_T_NPG);
|
||
} else {
|
||
DBG_INFO("[RAW] persist: no magic (first boot or lost), pg1 head: "
|
||
"%02X %02X %02X %02X %02X %02X %02X %02X",
|
||
(unsigned int)s_copy_buf[0], (unsigned int)s_copy_buf[1],
|
||
(unsigned int)s_copy_buf[2], (unsigned int)s_copy_buf[3],
|
||
(unsigned int)s_copy_buf[4], (unsigned int)s_copy_buf[5],
|
||
(unsigned int)s_copy_buf[6], (unsigned int)s_copy_buf[7]);
|
||
}
|
||
|
||
/* ---- 2) 擦除试验:擦 A/B 块 + 读回全 FF 校验 ---- */
|
||
erase_a = gd5f_block_erase(RAW_T_BLK_A);
|
||
erase_b = gd5f_block_erase(RAW_T_BLK_B);
|
||
|
||
for (k = 0u; k < RAW_T_NPG; k++) {
|
||
uint32_t i;
|
||
uint8_t all_ff = 1u;
|
||
|
||
pg = (RAW_T_BLK_A * GD5F_PAGES_PER_BLOCK) + RAW_T_PAGE0 + k;
|
||
if (raw_t_read(pg) != GD5F_OK) {
|
||
break;
|
||
}
|
||
for (i = 0; i < FTL_PAGE_SIZE; i++) {
|
||
if (s_copy_buf[i] != 0xFFu) {
|
||
all_ff = 0u;
|
||
break;
|
||
}
|
||
}
|
||
if (all_ff != 0u) {
|
||
ff_a++;
|
||
}
|
||
}
|
||
|
||
for (k = 0u; k < RAW_T_NPG; k++) {
|
||
uint32_t i;
|
||
uint8_t all_ff = 1u;
|
||
|
||
pg = (RAW_T_BLK_B * GD5F_PAGES_PER_BLOCK) + RAW_T_PAGE0 + k;
|
||
if (raw_t_read(pg) != GD5F_OK) {
|
||
break;
|
||
}
|
||
for (i = 0; i < FTL_PAGE_SIZE; i++) {
|
||
if (s_copy_buf[i] != 0xFFu) {
|
||
all_ff = 0u;
|
||
break;
|
||
}
|
||
}
|
||
if (all_ff != 0u) {
|
||
ff_b++;
|
||
}
|
||
}
|
||
|
||
/* ---- 3) 编程试验:写新 seq 图案 + 立即回读比对 ---- */
|
||
seq_new = (magic_ok != 0u) ? (seq_prev + 1u) : 0u;
|
||
for (k = 0u; k < RAW_T_NPG; k++) {
|
||
int ret;
|
||
|
||
pg = (RAW_T_BLK_A * GD5F_PAGES_PER_BLOCK) + RAW_T_PAGE0 + k;
|
||
raw_t_fill(s_copy_buf, seq_new, k);
|
||
if (gd5f_write_enable() != GD5F_OK) {
|
||
break;
|
||
}
|
||
ret = gd5f_program_load(0, s_copy_buf, FTL_PAGE_SIZE);
|
||
if (ret != GD5F_OK) {
|
||
break;
|
||
}
|
||
ret = gd5f_program_exec(pg);
|
||
if (ret != GD5F_OK) {
|
||
DBG_INFO("[RAW] prog exec pg %lu rc %d",
|
||
(unsigned long)pg, ret);
|
||
break;
|
||
}
|
||
if (raw_t_read(pg) != GD5F_OK) {
|
||
break;
|
||
}
|
||
if (raw_t_cmp(s_copy_buf, seq_new, k, &off) == 0) {
|
||
wv++;
|
||
} else {
|
||
DBG_ERROR("[RAW] write verify mismatch pg %lu off %lu exp %02X got %02X",
|
||
(unsigned long)(RAW_T_PAGE0 + k),
|
||
(unsigned long)off,
|
||
(unsigned int)raw_t_byte(seq_new, k, off),
|
||
(unsigned int)s_copy_buf[off]);
|
||
}
|
||
}
|
||
|
||
/* ---- 4) 汇总(单行速读) ---- */
|
||
DBG_INFO("[RAW] erase A=%d B=%d; ffA=%u/%lu ffB=%u/%lu; write seq=%lu wv=%u/%lu",
|
||
erase_a, erase_b,
|
||
(unsigned int)ff_a, (unsigned long)RAW_T_NPG,
|
||
(unsigned int)ff_b, (unsigned long)RAW_T_NPG,
|
||
(unsigned long)seq_new, (unsigned int)wv, (unsigned long)RAW_T_NPG);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:转储某 LPN 的 map 绑定与绑定页裸读内容(诊断:跨 boot 对比
|
||
* 绑定地址是否分歧、绑定页内容是否丢失/被擦)
|
||
* 入口参数:lpn - 逻辑页号(journal LPN = FatFs 页号)uint32_t
|
||
* tag - 场景标记(如 "fresh" / "postreset")const char*
|
||
* 返回值:无
|
||
* 限定条件:FTL 已初始化
|
||
* 函数说明:dhara_map_find 取当前树绑定 → 物理页号 → 绕开缓存的裸读
|
||
* 前 16 字节。两 boot 的同一 LPN:地址不同 = 树分歧;地址同
|
||
* 内容不同 = 数据页未落盘或被擦。
|
||
*/
|
||
void nand_ftl_dump_binding(uint32_t lpn, const char *tag) {
|
||
dhara_page_t loc = DHARA_PAGE_NONE;
|
||
dhara_error_t err;
|
||
uint8_t b[16];
|
||
uint32_t phy;
|
||
|
||
if (!s_initialized) {
|
||
return;
|
||
}
|
||
|
||
if (dhara_map_find(&s_map, lpn, &loc, &err) < 0) {
|
||
DBG_INFO("bind %s LPN=%lu: not found (err %d)",
|
||
tag, (unsigned long)lpn, (int)err);
|
||
return;
|
||
}
|
||
|
||
phy = ftl_phy_page(loc);
|
||
if (gd5f_page_read(phy) != GD5F_OK) {
|
||
DBG_INFO("bind %s LPN=%lu -> jpage %lu: raw read err",
|
||
tag, (unsigned long)lpn, (unsigned long)loc);
|
||
return;
|
||
}
|
||
gd5f_read_from_cache(0, b, sizeof(b));
|
||
DBG_INFO("bind %s LPN=%lu -> jpage %lu (phy %lu): "
|
||
"%02X %02X %02X %02X %02X %02X %02X %02X",
|
||
tag, (unsigned long)lpn, (unsigned long)loc,
|
||
(unsigned long)phy,
|
||
(unsigned int)b[0], (unsigned int)b[1],
|
||
(unsigned int)b[2], (unsigned int)b[3],
|
||
(unsigned int)b[4], (unsigned int)b[5],
|
||
(unsigned int)b[6], (unsigned int)b[7]);
|
||
}
|