029437be89
步骤 3/4(IWDG V2/V4/V5 + 堆低水位 S11):
- V2 任务饿死 stall 后 ≤2.5s IWDG 复位;V4 关中断死循环 ~4.9s 稳定复位;
V5 30min 120×FTP 全 PASS、零 IWDG 复位。堆告警全程未触发(min_ever=4728B)。
- task_watchdog.c:stall 证据写盘会经 nand_ftl_wd_feed 喂狗 → 增 s_stall_evidenced
每次 stall 仅取一次证据,避免"取证据"本身阻止复位。
- default_task.c:堆低水位告警(HEAP_LOW_WATER_B=3072)。
步骤 5(GD5F 错误通道 S9):gd5f2gq5ue.c/.h + nand_ftl.c 错误码统一为 int32_t 并
全量检查返回值;套件回归 16/16 PASSED。
- test/run_all_suites.py:--forbidden 改为按套件覆盖(GD5F 用 "HardFault;assert")——
TC-GD5F-704 故意触发 [ERR] 回落轮询,统一默认值会误判 FAIL。
步骤 6(网络稳定性):7.1 正常路径 ✅(V5 证据);7.2 N-2 丢弃可观测性代码已具备、
49s 静默在 V5 未复现(降级低风险观察项);7.3 N-3 待 PC peer。
步骤 7(工程卫生):
- S5:新增 test-cppcheck 钩子(仅 warning,排除模拟器桩)。
- S4:新增 scripts/check_test_traceability.py(默认仅告警)。
- 待核实项 TEST_SUITE_SELECTED 实机核实 = 死代码(无使用点);RTC 套件实际正常执行,
真缺口 = 缺套件级汇总行 → sd2506_test_task.c 补 === RTC Tests: N/M PASSED === 并
修正 Phase 级汇总累计问题;test_config.h 补齐 RTC 并注释"当前无引用"。
验证:产品 @build 0/0;pre-commit --all-files 17/17 Passed;
run_all_suites.py --suites RTC → PASS。
文档:实机测试实施指南 / SD2506_Test_Guide / 待处理内容 / 测试与架构改进建议 同步。
456 lines
14 KiB
C
456 lines
14 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 的证据
|
||
*/
|
||
|
||
/* 头文件包含区 */
|
||
#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;
|
||
|
||
/* ======================== 私有函数声明 ======================== */
|
||
|
||
static int ftl_flush_cache(void);
|
||
static int ftl_read_page(dhara_sector_t lpn);
|
||
|
||
/* 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;
|
||
return GD5F_OK;
|
||
}
|
||
|
||
/* ======================== dhara NAND HAL ======================== */
|
||
|
||
int dhara_nand_is_bad(const struct dhara_nand *n, dhara_block_t b) {
|
||
(void)n;
|
||
return gd5f2gq5ue_is_block_bad(ftl_phy_block(b));
|
||
}
|
||
|
||
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);
|
||
|
||
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 05) */
|
||
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];
|
||
|
||
if (gd5f_read_from_cache(0, buf, sizeof(buf)) != GD5F_OK) {
|
||
/* E-42 同族(2026-10-03):读失败 → buf 可能为陈旧/垃圾内容,
|
||
* 保守判为非空(块不可复用),避免把"没读到内容的块"判成空闲。 */
|
||
fre = 0;
|
||
} else {
|
||
uint32_t i;
|
||
|
||
fre = 1;
|
||
for (i = 0; i < sizeof(buf); i++) {
|
||
if (buf[i] != 0xFF) {
|
||
fre = 0;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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);
|
||
|
||
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;
|
||
}
|
||
|
||
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);
|
||
|
||
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 (err=%d), creating fresh", 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 (err=%d), volume kept, "
|
||
"storage unavailable", 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() != 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;
|
||
}
|