From cf33171c8fb0445b973da33b88d514c3baded097 Mon Sep 17 00:00:00 2001 From: wandering1 <1624155937@qq.com> Date: Thu, 27 Aug 2026 18:22:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AD=98=E5=82=A8=E6=B5=8B=E8=AF=95=E9=80=9A?= =?UTF-8?q?=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 10 + Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c | 444 ++++++++++++++-- Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h | 96 +++- Drivers/BSP/GD5F2GQ5UE/nand_ftl.c | 21 +- Drivers/BSP/GD5F2GQ5UE/nand_ftl.h | 1 + MDK-ARM/STM32F407-Demo.uvprojx | 10 + MDK-ARM/build_log.txt | 76 +-- Src/freertos.c | 126 +---- docs/GD5F2GQ5UE_Test_Guide.md | 741 +++++++++++++++++++++++++++ docs/GD5F2GQ5UE_Trap_Records.md | 104 ++++ docs/STM32F4-Base存储架构说明.md | 105 +++- test/ch395f_test.h | 2 +- test/ch395f_test_task.c | 51 +- test/gd5f_test.h | 89 ++++ test/gd5f_test_task.c | 750 ++++++++++++++++++++++++++++ test/net_test_task.c | 148 +++--- test/storage_test.h | 57 +++ test/storage_test_task.c | 384 ++++++++++++++ test/test_config.h | 73 +++ 19 files changed, 2952 insertions(+), 336 deletions(-) create mode 100644 docs/GD5F2GQ5UE_Test_Guide.md create mode 100644 test/gd5f_test.h create mode 100644 test/gd5f_test_task.c create mode 100644 test/storage_test.h create mode 100644 test/storage_test_task.c create mode 100644 test/test_config.h diff --git a/AGENTS.md b/AGENTS.md index 19553dd..153a321 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,3 +82,13 @@ Src/main.c → 外设初始化 → app_main_init → gd5f2gq5ue_init → tpafe51 - CH395F SPI PCB设计问题,现在分频系数必须为 8,速度相对较低 - FreeRTOS V10.3.1 via CMSIS-RTOS V2,HAL 时基用 TIM7(非 SysTick) - NVIC 优先级分组 4 位,外设中断优先级 ≥ 5(`configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY`) + +## 源码编辑与编码安全(铁律) + +本工程源码含大量中文注释,曾因错误的文本重写方式导致整文件乱码(`U+FFFD` 替换符,不可逆)。以下规则必须遵守: + +- **绝不用 `Get-Content` / `Set-Content` / `cmd` 文本重写含中文的源码文件**。这类"整文件重写"会按系统默认编码重新解码再写回,多字节中文必被 `U+FFFD` 替换且不可逆。 +- **改代码只用 Edit 工具**(精确子串替换,不重新编解码整个文件,安全);批量替换用 Edit 的 `replaceAll`。 +- 必须脚本级替换时,使用显式且一致的编码(如 Python `open(p, encoding="utf-8")` 同编码回写),并先确认工程编码(Keil 工程常用 GBK)——但**最稳的是别碰**,只用 Edit / git。 +- **发现乱码先别急着 `git checkout`**:`git checkout` 会丢弃所有未提交工作,使损失扩大。先评估能否用 Edit 仅改坏掉的那几行。 +- 验证是否真损坏用字节级检查(统计 `EF BF BD` 出现次数),不要依赖终端/Read 渲染判断。 diff --git a/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c b/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c index 4461c54..13c68aa 100644 --- a/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c +++ b/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c @@ -1,9 +1,13 @@ /* * 模块名称:GD5F2GQ5UE SPI NAND Flash 驱动 - * 模块功能:提�?GD5F2GQ5UE SPI NAND Flash 的初始化、读、写、擦除接�? * 适用平台:STM32F407ZGT6 + SPI1 硬件 SPI - * 作者:王建�? * 创建日期�?026-07-16 - * 修改记录�? * 2026-07-16 王建�? 创建初始版本 - * 2026-07-17 王建�? 切换硬件 SPI,修正擦除地址,参�?NuttX 驱动 + * 模块功能:提供 GD5F2GQ5UE SPI NAND Flash 的初始化、读、写、擦除接口 + * 适用平台:STM32F407ZGT6 + SPI1 硬件 SPI + * 作者:王建锋 + * 创建日期:2026-07-16 + * 修改记录: + * 2026-07-16 王建锋 创建初始版本 + * 2026-07-17 王建锋 切换硬件 SPI,修正擦除地址,参考 NuttX 驱动 + * 2026-08-27 补充函数注释(代码规范 V1.0);修正工厂坏块扫描为首页;增加 BBT 持久化 */ /* 头文件包含区 */ @@ -23,6 +27,18 @@ /* BBT 静态数组(1 bit 表示一个块,0=好块 1=坏块) */ static uint8_t s_bbt[GD5F_BBT_SIZE]; +/* BBT 持久化:保留块池(从 NAND 顶部向下挑选出厂好块,存储 BBT 副本) */ +#define GD5F_BBT_HDR_SIZE 16 +static uint32_t s_bbt_pool_blocks[GD5F_BBT_POOL_COUNT]; /* BBT 保留池块号列表(出厂好块) */ +static uint32_t s_bbt_pool_count = 0; /* 实际可用池块数(<= GD5F_BBT_POOL_COUNT) */ +static uint32_t s_usable_blocks = GD5F_TOTAL_BLOCKS; /* dhara 可见块数(= 最低池块号) */ +static uint32_t s_bbt_write_slot = 0; /* 下一写入槽(轮转指针) */ +static uint32_t s_bbt_version = 0; /* 当前持久化 BBT 版本号 */ +static uint8_t s_bbt_slot_dead = 0; /* 池块损坏位掩码 */ +static uint8_t s_bbt_page_buf[GD5F_PAGE_SIZE]; /* BBT 读写页缓冲(静态,避免占栈) */ +static uint8_t s_bbt_best[GD5F_BBT_SIZE]; /* 最高版本位图缓存 */ +static const uint8_t s_bbt_magic[4] = { 'G', 'B', 'B', 'T' }; /* BBT 存储魔数 "GBBT" */ + /* 外部 SPI 句柄声明 */ extern SPI_HandleTypeDef hspi1; @@ -34,6 +50,10 @@ volatile uint8_t g_spi1_dma_done; /* * 函数功能:SPI1 TX DMA 完成回调(PROGRAM_LOAD 数据发送用) + * 入口参数:hspi - 触发回调的 SPI 句柄 SPI_HandleTypeDef* + * 返回值:无 + * 限定条件:由 HAL DMA 中断调用 + * 函数说明:仅当 SPI1 传输完成时置位 g_spi1_dma_done 标志 */ void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { @@ -59,6 +79,13 @@ static int gd5f_page_program(uint32_t page_addr, uint16_t column, const uint8_t *p_buf, size_t size); static int gd5f_set_feature(uint8_t addr, uint8_t data); +/* BBT 持久化内部函数 */ +static int gd5f_private_bbt_locate_pool(void); +static uint32_t gd5f_private_crc32(const uint8_t *p_buf, uint32_t len); +static int gd5f_private_bbt_find_best(void); +static int gd5f_bbt_load(void); +static int gd5f_bbt_save(void); + /* ======================== SPI 原语定义 ======================== */ /* @@ -95,7 +122,7 @@ int gd5f_wait_busy(uint32_t timeout_ms) { * 入口参数:无 * 返回值:0 - 成功 * 限定条件:SPI 已初始化 - * 函数说明:CS# 拉低后发�?06h 命令再拉�? */ + * 函数说明:CS# 拉低后发 06h 命令再拉高 */ int gd5f_write_enable(void) { uint8_t cmd = GD5F_CMD_WRITE_ENABLE; @@ -108,10 +135,10 @@ int gd5f_write_enable(void) { /* * 函数功能:读取状态寄存器 - * 入口参数:p_status - 状态值输出指�? uint8_t* 不为 NULL + * 出口参数:p_status - 状态值输出指针 uint8_t* 不为 NULL * 返回值:0 - 成功 * 限定条件:SPI 已初始化 - * 函数说明:发�?0Fh + C0h 地址后读�?字节状态�? */ + * 函数说明:发 0Fh + C0h 地址后读 1 字节状态 */ int gd5f_read_status(uint8_t *p_status) { uint8_t cmd = GD5F_CMD_GET_FEATURE; uint8_t addr = GD5F_REG_STATUS; @@ -126,9 +153,11 @@ int gd5f_read_status(uint8_t *p_status) { } /* - * 函数功能:页读取(将数据从存储阵列加载到内部缓存�? * 入口参数:page_addr - 页地址 uint32_t - * 返回值:0 - 成功,其�?- 错误�? * 限定条件:SPI 已初始化 - * 函数说明:发�?13h + 3字节行地址,等�?OIP 清零 + * 函数功能:页读取(将数据从存储阵列加载到内部缓存) + * 入口参数:page_addr - 页地址 uint32_t 0 ~ GD5F_TOTAL_BLOCKS*64-1 + * 返回值:0 - 成功,其它 - 错误 + * 限定条件:SPI 已初始化 + * 函数说明:发 13h + 3字节行地址,等待 OIP 清零 */ int gd5f_page_read(uint32_t page_addr) { uint8_t cmd[4] = {0}; @@ -148,11 +177,11 @@ int gd5f_page_read(uint32_t page_addr) { /* * 函数功能:从内部缓存读取数据 * 入口参数:column - 列地址(页内偏移) uint16_t 0 - 2047 - * p_buf - 数据输出缓冲�? uint8_t* 不为 NULL - * size - 读取字节�? size_t > 0 + * size - 读取字节数 size_t > 0 + * 出口参数:p_buf - 数据输出缓冲区 uint8_t* 不为 NULL * 返回值:0 - 成功 * 限定条件:必须先调用 gd5f_page_read 完成数据加载 - * 函数说明:发�?0Bh + 2字节列地址 +1字节 dummy 后读取数�? */ + * 函数说明:发 0Bh + 2字节列地址 +1字节 dummy 后读取数据 */ int gd5f_read_from_cache(uint16_t column, uint8_t *p_buf, size_t size) { uint8_t cmd[4] = {0}; @@ -191,11 +220,11 @@ int gd5f_read_from_cache(uint16_t column, uint8_t *p_buf, * 函数功能:页编程(将数据写入指定页) * 入口参数:page_addr - 页地址 uint32_t * column - 列地址 uint16_t 0 - 2047 - * p_buf - 数据缓冲�? const uint8_t* - * size - 写入字节�? size_t > 0 - * 返回值:0 - 成功,其�?- 错误�? * 限定条件:目标区域已擦除 - * 函数说明�?. 写使�?- 02h 加载数据 - 10h 执行编程 - 等待完成 - * 2. 编程完成后检�?P_FAIL �? */ + * p_buf - 数据缓冲区 const uint8_t* + * size - 写入字节数 size_t > 0 + * 返回值:0 - 成功,其它 - 错误 * 限定条件:目标区域已擦除 + * 函数说明:1. 写使能 - 02h 加载数据 - 10h 执行编程 - 等待完成 + * 2. 编程完成后检查 P_FAIL 位 */ static int gd5f_page_program(uint32_t page_addr, uint16_t column, const uint8_t *p_buf, size_t size) { int ret; @@ -205,6 +234,15 @@ static int gd5f_page_program(uint32_t page_addr, uint16_t column, return gd5f_program_exec(page_addr); } +/* + * 函数功能:将数据加载到内部缓存(PROGRAM LOAD) + * 入口参数:column - 列地址(页内偏移) uint16_t 0 - 2047 + * p_buf - 待写入数据缓冲区 const uint8_t* 不为 NULL + * size - 写入字节数 size_t > 0 + * 返回值:0 - 成功,其它 - 错误 + * 限定条件:SPI 已初始化,目标区域已擦除 + * 函数说明:发 02h + 2字节列地址后发送数据;超过 DMA 阈值走 DMA + */ int gd5f_program_load(uint16_t column, const uint8_t *p_buf, size_t size) { uint8_t cmd[3]; cmd[0] = GD5F_CMD_PROGRAM_LOAD; @@ -237,6 +275,13 @@ int gd5f_program_load(uint16_t column, const uint8_t *p_buf, size_t size) { return GD5F_OK; } +/* + * 函数功能:执行页编程(PROGRAM EXECUTE) + * 入口参数:page_addr - 目标页地址 uint32_t 0 ~ GD5F_TOTAL_BLOCKS*64-1 + * 返回值:0 - 成功,其它 - 错误(GD5F_PROGRAM_FAIL 等) + * 限定条件:已先调用 gd5f_program_load 加载数据 + * 函数说明:发 10h + 3字节页地址触发编程,等待完成后检查 P_FAIL 位 + */ int gd5f_program_exec(uint32_t page_addr) { int ret; uint8_t cmd[4]; @@ -260,6 +305,13 @@ int gd5f_program_exec(uint32_t page_addr) { return GD5F_OK; } +/* + * 函数功能:检查上次读操作的 ECC 状态 + * 入口参数:无 + * 返回值:0 - 无 ECC 错误,GD5F_ECC_ERROR - 存在不可纠正 ECC 错误 + * 限定条件:SPI 已初始化,ECC 已使能 + * 函数说明:读取状态寄存器 ECCS[5:4] 位,0x02 表示不可纠正错误 + */ int gd5f_check_ecc(void) { uint8_t status; gd5f_read_status(&status); @@ -269,11 +321,12 @@ int gd5f_check_ecc(void) { } /* - * 函数功能:设�?Feature 寄存�? * 入口参数:addr - 寄存器地址 uint8_t + * 函数功能:设置 Feature 寄存器 + * 入口参数:addr - 寄存器地址 uint8_t * data - 写入数据 uint8_t * 返回值:0 - 成功 * 限定条件:SPI 已初始化 - * 函数说明:先写使能,发�?1Fh + 地址 + 数据,等待操作完�? */ + * 函数说明:先写使能,发 1Fh + 地址 + 数据,等待操作完成 */ static int gd5f_set_feature(uint8_t addr, uint8_t data) { uint8_t cmd[3]; @@ -334,8 +387,8 @@ int gd5f_block_erase(uint32_t block_addr) { * 入口参数:无 * 返回值:0 - 成功 * 限定条件:SPI 已初始化,ECC 尚未使能 - * 函数说明:读取每个块最后一页(page 63)的 spare byte 0, - * 若不为 0xFF 则为出厂坏块 + * 函数说明:读取每个块首页(page 0)的 spare byte 0, + * 若不为 0xFF 则为出厂坏块(符合 datasheet §12.4) */ static int gd5f_bbt_scan(void) { uint32_t block; @@ -345,7 +398,7 @@ static int gd5f_bbt_scan(void) { memset(s_bbt, 0, GD5F_BBT_SIZE); for (block = 0; block < GD5F_TOTAL_BLOCKS; block++) { - uint32_t page = (block << 6) | 63; + uint32_t page = block << 6; gd5f_page_read(page); gd5f_read_from_cache(GD5F_PAGE_SIZE, spare, 1); @@ -360,24 +413,57 @@ static int gd5f_bbt_scan(void) { return GD5F_OK; } -void gd5f2gq5ue_bbt_rescan(void) { - uint32_t blk; - uint8_t spare[64]; - memset(s_bbt, 0, GD5F_BBT_SIZE); - for (blk = 0; blk < GD5F_TOTAL_BLOCKS; blk++) { - uint32_t page = (blk << 6) | 63; - gd5f_page_read(page); - gd5f_read_from_cache(GD5F_PAGE_SIZE, spare, sizeof(spare)); - if (spare[0] != 0xFF) { - s_bbt[blk >> 3] |= (1 << (blk & 7)); - } - } -} - +/* + * 函数功能:清空 RAM 中的 BBT + * 入口参数:无 + * 返回值:无 + * 限定条件:无 + * 函数说明:仅清空内存位图,不影响闪存中的持久化副本 + */ void gd5f2gq5ue_bbt_clear(void) { memset(s_bbt, 0, GD5F_BBT_SIZE); } +/* + * 函数功能:重建 BBT(重新扫描出厂坏块并持久化,覆盖运行时注入的坏块) + * 入口参数:无 + * 返回值:GD5F_OK - 成功,其他 - 错误码 + * 限定条件:gd5f2gq5ue_init() 已成功调用 + * 函数说明:临时关闭 ECC 重新扫描每块 page0 spare[0] 的出厂坏块标记,覆盖 RAM BBT, + * 再写回持久化 BBT 池(版本 +1,掉电安全),从而清除 TC-STO-03 等运行时 + * 标记的坏块。调用后建议再执行 nand_ftl_format() 清空 dhara map。 + */ +int gd5f2gq5ue_bbt_rebuild(void) { + DBG_INFO("Rebuilding BBT from factory scan..."); + + /* 1. 关闭 ECC 以读取原始出厂坏块标记(与 gd5f2gq5ue_init 的扫描前提一致) */ + gd5f_set_feature(0xB0, 0x00); + gd5f_wait_busy(100); + + /* 2. 重新扫描出厂坏块(覆盖 RAM BBT,剔除运行时注入的坏块) */ + gd5f_bbt_scan(); + + /* 3. 重新使能 ECC */ + gd5f_set_feature(0xB0, 0x10); + gd5f_wait_busy(100); + + /* 4. 持久化重建后的 BBT(覆盖持久化区中的脏副本) */ + if (gd5f_bbt_save() != GD5F_OK) { + DBG_ERROR("BBT rebuild: persist failed"); + return GD5F_ERROR; + } + + DBG_INFO("BBT rebuilt and persisted"); + return GD5F_OK; +} + +/* + * 函数功能:通过调试串口打印当前 BBT 统计与坏块列表 + * 入口参数:无 + * 返回值:无 + * 限定条件:gd5f2gq5ue_init() 已成功调用 + * 函数说明:最多打印前 32 个坏块块号与偏移 + */ void gd5f2gq5ue_print_bbt(void) { uint32_t bad_count = 0; for (uint32_t b = 0; b < GD5F_TOTAL_BLOCKS; b++) { @@ -405,9 +491,10 @@ void gd5f2gq5ue_print_bbt(void) { * 限定条件:SPI1 和相关 GPIO 已由 CubeMX 初始化完成 * 函数说明:1. 发送复位命令并等待完成 * 2. 读取芯片 ID 并校验 - * 3. 扫描出厂坏块构建 BBT(ECC 使能前) + * 3. 扫描出厂坏块构建 BBT(ECC 使能前,读每块首页 spare[0]) * 4. 使能内部 ECC (B0h bit4) * 5. 解除所有块保护 (A0h = 0x00) + * 6. 定位保留块池并加载持久化 BBT(ECC 使能后) */ int gd5f2gq5ue_init(void) { int ret = GD5F_OK; @@ -455,16 +542,23 @@ int gd5f2gq5ue_init(void) { gd5f_set_feature(GD5F_REG_PROTECT, 0x00); gd5f_wait_busy(100); + DBG_INFO("Locating BBT pool..."); + gd5f_private_bbt_locate_pool(); + + DBG_INFO("Loading persisted BBT..."); + gd5f_bbt_load(); + DBG_INFO("NAND init OK"); return GD5F_OK; } /* - * 函数功能:读取芯�?ID(MID + DID�? * 入口参数:mid - 制造商 ID 输出指针 uint8_t* 不为 NULL - * did - 设备 ID 输出指针 uint8_t* 不为 NULL + * 函数功能:读取芯片 ID(MID + DID) + * 出口参数:p_mid - 制造商 ID 输出指针 uint8_t* 不为 NULL + * p_did - 设备 ID 输出指针 uint8_t* 不为 NULL * 返回值:0 - 成功 * 限定条件:SPI 已初始化 - * 函数说明:发�?9Fh 命令后接�?�?dummy + MID + DID + * 函数说明:发 9Fh 命令后接 1 字节 dummy + MID + DID */ int gd5f2gq5ue_read_id(uint8_t *p_mid, uint8_t *p_did) { uint8_t cmd = GD5F_CMD_READ_ID; @@ -483,13 +577,20 @@ int gd5f2gq5ue_read_id(uint8_t *p_mid, uint8_t *p_did) { /* * 函数功能:从 NAND 读取数据(支持跨页) - * 入口参数:offset - 起始字节偏移 long 0 ~ 总容�?1 - * p_buf - 数据缓冲�? uint8_t* 不为 NULL - * size - 读取字节�? size_t > 0 - * 返回值:0 - 成功,其�?- 错误�? * 限定条件:gd5f2gq5ue_init() 已成功调�? * 函数说明:自动处理跨页读�? */ + * 入口参数:offset - 起始字节偏移 long 0 ~ 总容量-1 + * size - 读取字节数 size_t > 0 + * 出口参数:p_buf - 数据缓冲区 uint8_t* 不为 NULL + * 返回值:0 - 成功,其它 - 错误 + * 限定条件:gd5f2gq5ue_init() 已成功调用 + * 函数说明:自动处理跨页读取;offset+size 越界返回错误 + */ int gd5f2gq5ue_read(long offset, uint8_t *p_buf, size_t size) { int ret = GD5F_OK; + if (offset < 0 || (uint32_t)offset + (uint32_t)size > GD5F_TOTAL_SIZE) { + return GD5F_ERROR; + } + while (size > 0) { uint32_t page_addr = offset / GD5F_PAGE_SIZE; uint16_t column = offset % GD5F_PAGE_SIZE; @@ -519,13 +620,20 @@ int gd5f2gq5ue_read(long offset, uint8_t *p_buf, size_t size) { /* * 函数功能:向 NAND 写入数据(支持跨页) - * 入口参数:offset - 起始字节偏移 long 0 ~ 总容�?1 - * p_buf - 数据缓冲�? const uint8_t* 不为 NULL - * size - 写入字节�? size_t > 0 - * 返回值:0 - 成功,其�?- 错误�? * 限定条件:gd5f2gq5ue_init() 已成功调用,目标区域已擦�? * 函数说明:自动处理跨页写�? */ + * 入口参数:offset - 起始字节偏移 long 0 ~ 总容量-1 + * p_buf - 数据缓冲区 const uint8_t* 不为 NULL + * size - 写入字节数 size_t > 0 + * 返回值:0 - 成功,其它 - 错误 + * 限定条件:gd5f2gq5ue_init() 已成功调用,目标区域已擦除 + * 函数说明:自动处理跨页写入;offset+size 越界返回错误 + */ int gd5f2gq5ue_write(long offset, const uint8_t *p_buf, size_t size) { int ret = GD5F_OK; + if (offset < 0 || (uint32_t)offset + (uint32_t)size > GD5F_TOTAL_SIZE) { + return GD5F_ERROR; + } + while (size > 0) { uint32_t page_addr = offset / GD5F_PAGE_SIZE; uint16_t column = offset % GD5F_PAGE_SIZE; @@ -549,7 +657,12 @@ int gd5f2gq5ue_write(long offset, const uint8_t *p_buf, size_t size) { } /* - * 函数功能:擦除块(按块擦除,最小单�?128KB�? * 入口参数:offset - 起始字节偏移 long 必须块对�? * size - 擦除字节�? size_t 必须块大小整数�? * 返回值:0 - 成功,其�?- 错误�? * 限定条件:gd5f2gq5ue_init() 已成功调�? * 函数说明:擦除操作以块为单位 + * 函数功能:擦除块(按块擦除,最小单位 128KB) + * 入口参数:offset - 起始字节偏移 long 必须 GD5F_BLOCK_SIZE 对齐 + * size - 擦除字节数 size_t 必须 GD5F_BLOCK_SIZE 整数倍 + * 返回值:0 - 成功,其它 - 错误码 + * 限定条件:gd5f2gq5ue_init() 已成功调用 + * 函数说明:循环擦除 [offset, offset+size) 覆盖的每个块,偏移/长度不对齐返回错误 */ int gd5f2gq5ue_erase(long offset, size_t size) { int ret = GD5F_OK; @@ -577,10 +690,10 @@ int gd5f2gq5ue_erase(long offset, size_t size) { } /* - * 函数功能:复位芯�? * 入口参数:无 + * 函数功能:复位芯片 * 返回值:0 - 成功 * 限定条件:SPI 已初始化 - * 函数说明:发�?FFh 复位命令后等�?5ms + * 函数说明:发 FFh 复位命令后等待 5ms */ int gd5f2gq5ue_reset(void) { uint8_t cmd = GD5F_CMD_RESET; @@ -619,6 +732,231 @@ void gd5f2gq5ue_mark_block_bad(uint32_t block) { if (block >= GD5F_TOTAL_BLOCKS) { return; } - s_bbt[block >> 3] |= (1 << (block & 7)); + uint32_t byte = block >> 3; + uint8_t bit = (uint8_t)(1U << (block & 7)); + if (s_bbt[byte] & bit) { + return; /* 已是坏块,幂等,无需重复持久化 */ + } + s_bbt[byte] |= bit; + gd5f_bbt_save(); /* 持久化到保留块池(带版本号 + CRC) */ +} + +/* ======================== BBT 持久化实现 ======================== */ + +/* + * 函数功能:从 NAND 顶部向下挑选出厂好块作为 BBT 保留池 + * 入口参数:无 + * 返回值:0 - 成功 + * 限定条件:gd5f_bbt_scan() 已完成(s_bbt 已就绪) + * 函数说明:保留池不交给 dhara 管理;最低池块号以上的块全部排除, + * 并将可用块数 s_usable_blocks 设为最低池块号 + */ +static int gd5f_private_bbt_locate_pool(void) { + uint32_t cnt = 0; + uint32_t b = GD5F_TOTAL_BLOCKS; + + while (b-- > 0 && cnt < GD5F_BBT_POOL_COUNT) { + if (((s_bbt[b >> 3] >> (b & 7)) & 1) == 0) { + s_bbt_pool_blocks[cnt++] = b; + } + } + s_bbt_pool_count = cnt; + if (cnt > 0) { + s_usable_blocks = s_bbt_pool_blocks[cnt - 1]; /* 最低池块 = dhara 可见上限 */ + } + return GD5F_OK; +} + +/* + * 函数功能:CRC32 计算(IEEE 802.3,多项式 0xEDB88320) + * 入口参数:p_buf - 数据缓冲区 const uint8_t* 不为 NULL + * len - 数据长度 uint32_t > 0 + * 返回值:32 位 CRC 校验值 + * 限定条件:无 + * 函数说明:采用位级(bit-by-bit)实现,初始值 0xFFFFFFFF + */ +static uint32_t gd5f_private_crc32(const uint8_t *p_buf, uint32_t len) { + uint32_t crc = 0xFFFFFFFFU; + for (uint32_t i = 0; i < len; i++) { + crc ^= p_buf[i]; + for (int j = 0; j < 8; j++) { + uint32_t mask = (crc & 1U) ? 0xEDB88320U : 0U; + crc = (crc >> 1) ^ mask; + } + } + return ~crc; +} + +/* + * 函数功能:在保留池中找出版本号最高的有效 BBT 副本 + * 入口参数:无 + * 返回值:0 - 成功 + * 限定条件:gd5f_private_bbt_locate_pool() 已成功执行 + * 函数说明:校验 magic / len / CRC;结果缓存到 s_bbt_best / s_bbt_version, + * 并设置下一轮转写入槽 s_bbt_write_slot + */ +static int gd5f_private_bbt_find_best(void) { + uint32_t best_ver = 0; + int best_slot = -1; + + s_bbt_slot_dead = 0; + for (uint32_t i = 0; i < s_bbt_pool_count; i++) { + uint32_t block = s_bbt_pool_blocks[i]; + uint32_t page = block * GD5F_PAGES_PER_BLOCK; + uint32_t ver, len, crc; + + gd5f_page_read(page); + gd5f_read_from_cache(0, s_bbt_page_buf, GD5F_BBT_HDR_SIZE + GD5F_BBT_SIZE); + if (memcmp(s_bbt_page_buf, s_bbt_magic, 4) != 0) { + continue; + } + memcpy(&ver, s_bbt_page_buf + 4, 4); + memcpy(&len, s_bbt_page_buf + 8, 4); + memcpy(&crc, s_bbt_page_buf + 12, 4); + if (len != GD5F_BBT_SIZE) { + continue; + } + if (gd5f_private_crc32(s_bbt_page_buf + GD5F_BBT_HDR_SIZE, GD5F_BBT_SIZE) != crc) { + continue; + } + if (best_slot < 0 || (int32_t)ver > (int32_t)best_ver) { + best_ver = ver; + best_slot = (int)i; + memcpy(s_bbt_best, s_bbt_page_buf + GD5F_BBT_HDR_SIZE, GD5F_BBT_SIZE); + } + } + if (best_slot >= 0) { + s_bbt_version = best_ver; + s_bbt_write_slot = (uint32_t)(best_slot + 1) % s_bbt_pool_count; + } else { + s_bbt_write_slot = 0; + } + return GD5F_OK; +} + +/* + * 函数功能:加载持久化 BBT(OR 进 RAM BBT) + * 入口参数:无 + * 返回值:0 - 成功 + * 限定条件:gd5f_private_bbt_locate_pool() 已成功执行 + * 函数说明:工厂扫描已填好块,持久化副本补充运行期坏块 + */ +static int gd5f_bbt_load(void) { + gd5f_private_bbt_find_best(); + if (s_bbt_pool_count == 0) { + return GD5F_OK; + } + for (uint32_t i = 0; i < GD5F_BBT_SIZE; i++) { + s_bbt[i] |= s_bbt_best[i]; + } + return GD5F_OK; +} + +/* + * 函数功能:将当前 RAM BBT 持久化到保留池(轮转写入,掉电安全) + * 入口参数:无 + * 返回值:0 - 成功,GD5F_ERROR - 写入失败 + * 限定条件:gd5f_private_bbt_locate_pool() 已成功执行 + * 函数说明:版本号自增;先擦除目标块再编程;损坏块标记后跳到下一槽 + */ +static int gd5f_bbt_save(void) { + if (s_bbt_pool_count == 0) { + return GD5F_OK; + } + uint32_t slot = s_bbt_write_slot; + + while (s_bbt_slot_dead & (1U << slot)) { + slot = (slot + 1) % s_bbt_pool_count; + if (slot == s_bbt_write_slot) { + break; /* 所有池块均损坏 */ + } + } + + uint32_t block = s_bbt_pool_blocks[slot]; + uint32_t page = block * GD5F_PAGES_PER_BLOCK; + uint32_t ver = s_bbt_version + 1; + uint32_t len = GD5F_BBT_SIZE; + uint32_t crc = gd5f_private_crc32(s_bbt, GD5F_BBT_SIZE); + + if (gd5f_block_erase(block) != GD5F_OK) { + s_bbt_slot_dead |= (1U << slot); + s_bbt_write_slot = (slot + 1) % s_bbt_pool_count; + return GD5F_ERROR; + } + + memcpy(s_bbt_page_buf, s_bbt_magic, 4); + memcpy(s_bbt_page_buf + 4, &ver, 4); + memcpy(s_bbt_page_buf + 8, &len, 4); + memcpy(s_bbt_page_buf + 12, &crc, 4); + memcpy(s_bbt_page_buf + GD5F_BBT_HDR_SIZE, s_bbt, GD5F_BBT_SIZE); + + gd5f_write_enable(); + gd5f_program_load(0, s_bbt_page_buf, GD5F_BBT_HDR_SIZE + GD5F_BBT_SIZE); + gd5f_program_exec(page); + gd5f_wait_busy(100); + + s_bbt_version = ver; + s_bbt_write_slot = (slot + 1) % s_bbt_pool_count; + return GD5F_OK; +} + +/* + * 函数功能:获取 dhara 可用块数(= 最低保留池块号) + * 入口参数:无 + * 返回值:dhara 可见的块数量(= 保留块池起始块号) + * 限定条件:gd5f2gq5ue_init() 已成功调用 + * 函数说明:保留块池用于存储持久化 BBT,不交给 dhara 管理 + */ +uint32_t gd5f_get_usable_blocks(void) { + return s_usable_blocks; +} + +/* + * 函数功能:从闪存读取当前生效的 BBT 位图(测试/调试用) + * 入口参数:无 + * 出口参数:p_bbt - 位图输出缓冲 uint8_t* 不为 NULL,长度 >= GD5F_BBT_SIZE + * p_version - 版本号输出 uint32_t* 不为 NULL + * 返回值:0 - 成功,GD5F_ERROR - 参数非法 + * 限定条件:gd5f2gq5ue_init() 已成功调用 + * 函数说明:返回闪存中版本号最高的有效 BBT 拷贝(供测试验证持久化) + */ +int32_t gd5f_bbt_dump_flash(uint8_t *p_bbt, uint32_t *p_version) { + if (p_bbt == NULL || p_version == NULL) { + return GD5F_ERROR; + } + gd5f_private_bbt_find_best(); + memcpy(p_bbt, s_bbt_best, GD5F_BBT_SIZE); + *p_version = s_bbt_version; + return GD5F_OK; +} + +/* + * 函数功能:擦除保留块池(撤销持久化 BBT,回到仅工厂扫描态) + * 入口参数:无 + * 返回值:0 - 成功 + * 限定条件:gd5f2gq5ue_init() 已成功调用 + * 函数说明:仅供测试/出厂重置;调用方需自行还原 RAM BBT + */ +int32_t gd5f_bbt_wipe_pool(void) { + if (s_bbt_pool_count == 0) { + return GD5F_OK; + } + for (uint32_t i = 0; i < s_bbt_pool_count; i++) { + gd5f_block_erase(s_bbt_pool_blocks[i]); + } + s_bbt_write_slot = 0; + s_bbt_version = 0; + return GD5F_OK; +} + +/* + * 函数功能:从保留块池重新加载持久化 BBT 到 RAM(测试/调试用) + * 入口参数:无 + * 返回值:0 - 成功 + * 限定条件:gd5f2gq5ue_init() 已成功调用 + * 函数说明:将闪存中版本号最高的有效副本 OR 进 RAM BBT + */ +int32_t gd5f_bbt_reload(void) { + return gd5f_bbt_load(); } diff --git a/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h b/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h index f8ffde3..bb2f423 100644 --- a/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h +++ b/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h @@ -17,6 +17,7 @@ extern "C" { #endif #include "main.h" +#include /* ======================== 宏定义 ======================== */ @@ -60,6 +61,9 @@ extern "C" { #define GD5F_TOTAL_BLOCKS 2048 #define GD5F_TOTAL_SIZE (GD5F_TOTAL_BLOCKS * GD5F_BLOCK_SIZE) +/* 坏块表持久化:保留块池数量(从 NAND 顶部向下挑选出厂好块存储 BBT) */ +#define GD5F_BBT_POOL_COUNT 4 + /* 制造商 ID 和设备 ID */ #define GD5F_MANUFACTURER_ID 0xC8 #define GD5F_DEVICE_ID 0x52 @@ -91,7 +95,7 @@ extern "C" { * 3. 使能内部 ECC (B0h bit4) * 4. 解除所有块保护 (A0h = 0x00) */ -int gd5f2gq5ue_init(void); +int32_t gd5f2gq5ue_init(void); /* * 函数功能:读取芯片 ID(MID + DID) @@ -101,13 +105,13 @@ int gd5f2gq5ue_init(void); * 限定条件:SPI 已初始化 * 函数说明:发送 9Fh 命令后接收1个 dummy + MID + DID */ -int gd5f2gq5ue_read_id(uint8_t *p_mid, uint8_t *p_did); +int32_t gd5f2gq5ue_read_id(uint8_t *p_mid, uint8_t *p_did); /* * 函数功能:从 NAND 读取数据(支持跨页) - * 入口参数:offset - 起始字节偏移 long 0 - GD5F_TOTAL_SIZE-1 + * 入口参数:offset - 起始字节偏移 int32_t 0 - GD5F_TOTAL_SIZE-1 * buf - 数据缓冲区 uint8_t* 不为 NULL - * size - 读取字节数 size_t > 0 + * size - 读取字节数 uint32_t > 0 * 返回值:0 - 成功,其他 - 错误码 * 限定条件:gd5f2gq5ue_init() 已成功调用 * 函数说明:自动处理跨页读取,每次读取不超过当前页剩余空间 @@ -116,9 +120,9 @@ int gd5f2gq5ue_read(long offset, uint8_t *buf, size_t size); /* * 函数功能:向 NAND 写入数据(支持跨页) - * 入口参数:offset - 起始字节偏移 long 0 - GD5F_TOTAL_SIZE-1 + * 入口参数:offset - 起始字节偏移 int32_t 0 - GD5F_TOTAL_SIZE-1 * buf - 数据缓冲区 uint8_t* 不为 NULL - * size - 写入字节数 size_t > 0 + * size - 写入字节数 uint32_t > 0 * 返回值:0 - 成功,其他 - 错误码 * 限定条件:gd5f2gq5ue_init() 已成功调用,目标区域已擦除 * 函数说明:自动处理跨页写入,每次写入不超过当前页剩余空间 @@ -127,8 +131,8 @@ int gd5f2gq5ue_write(long offset, const uint8_t *buf, size_t size); /* * 函数功能:擦除块(按块擦除,最小单位 128KB) - * 入口参数:offset - 起始字节偏移 long 必须 GD5F_BLOCK_SIZE 对齐 - * size - 擦除字节数 size_t 必须 GD5F_BLOCK_SIZE 整数倍 + * 入口参数:offset - 起始字节偏移 int32_t 必须 GD5F_BLOCK_SIZE 对齐 + * size - 擦除字节数 uint32_t 必须 GD5F_BLOCK_SIZE 整数倍 * 返回值:0 - 成功,其他 - 错误码 * 限定条件:gd5f2gq5ue_init() 已成功调用 * 函数说明:擦除操作以块为单位,offset 和 size 必须块对齐 @@ -142,7 +146,7 @@ int gd5f2gq5ue_erase(long offset, size_t size); * 限定条件:SPI 已初始化 * 函数说明:发送 FFh 复位命令后等待 5ms */ -int gd5f2gq5ue_reset(void); +int32_t gd5f2gq5ue_reset(void); /* * 函数功能:查询块是否坏块 @@ -151,31 +155,81 @@ int gd5f2gq5ue_reset(void); * 限定条件:gd5f2gq5ue_init() 已成功调用 * 函数说明:通过 BBT(Bad Block Table)查询,BBT 在 init 阶段 ECC 使能前扫描构建 */ -int gd5f2gq5ue_is_block_bad(uint32_t block); +int32_t gd5f2gq5ue_is_block_bad(uint32_t block); /* * 函数功能:标记块为坏块 * 入口参数:block - 块编号 uint32_t 0 ~ GD5F_TOTAL_BLOCKS-1 * 返回值:无 * 限定条件:gd5f2gq5ue_init() 已成功调用 - * 函数说明:更新 BBT,并尝试物理标记该块最后一页 spare byte 0 为 0x00 + * 函数说明:更新 RAM 中的 BBT,并持久化到保留块池(带版本号 + CRC, + * 轮转写入,掉电安全);仅在管理区内的块才纳入持久化 BBT */ void gd5f2gq5ue_mark_block_bad(uint32_t block); void gd5f2gq5ue_bbt_clear(void); -void gd5f2gq5ue_bbt_rescan(void); void gd5f2gq5ue_print_bbt(void); +/* + * 函数功能:重建 BBT(重新扫描出厂坏块并持久化,覆盖运行时注入的坏块) + * 入口参数:无 + * 返回值:GD5F_OK - 成功,其他 - 错误码 + * 限定条件:gd5f2gq5ue_init() 已成功调用 + * 函数说明:用于测试(如 TC-STO-03 注入坏块后)或出厂重置后恢复。 + * 临时关闭 ECC 重新扫描每块 page0 spare[0] 的出厂坏块标记,覆盖 RAM BBT, + * 再写回持久化 BBT 池(版本 +1,掉电安全),从而清除运行时 mark 的坏块。 + * 调用后建议再执行 nand_ftl_format() 清空 dhara map。 + */ +int gd5f2gq5ue_bbt_rebuild(void); + +/* + * 函数功能:获取 dhara 可用块数(总块数减去保留块池) + * 入口参数:无 + * 返回值:dhara 可见的块数量(= 保留块池起始块号) + * 限定条件:gd5f2gq5ue_init() 已成功调用 + * 函数说明:保留块池用于存储持久化 BBT,不交给 dhara 管理 + */ +uint32_t gd5f_get_usable_blocks(void); + +/* + * 函数功能:从闪存读取当前生效的 BBT 位图(测试/调试用) + * 入口参数:p_bbt - 位图输出缓冲 uint8_t* 不为 NULL,长度 >= GD5F_BBT_SIZE + * p_version - 版本号输出 uint32_t* 不为 NULL + * 返回值:0 - 成功,其他 - 错误码 + * 限定条件:gd5f2gq5ue_init() 已成功调用 + * 函数说明:返回闪存中版本号最高的有效 BBT 拷贝(供测试验证持久化) + */ +int32_t gd5f_bbt_dump_flash(uint8_t *p_bbt, uint32_t *p_version); + +/* + * 函数功能:擦除保留块池(撤销持久化 BBT,回到仅工厂扫描态) + * 入口参数:无 + * 返回值:0 - 成功 + * 限定条件:gd5f2gq5ue_init() 已成功调用 + * 函数说明:仅供测试/出厂重置使用;擦除后下次启动 load 找不到副本, + * 仅保留 RAM 中工厂扫描结果(调用方需自行还原 RAM BBT)。 + */ +int32_t gd5f_bbt_wipe_pool(void); + +/* + * 函数功能:从保留块池重新加载持久化 BBT 到 RAM(测试/调试用) + * 入口参数:无 + * 返回值:0 - 成功 + * 限定条件:gd5f2gq5ue_init() 已成功调用 + * 函数说明:将闪存中版本号最高的有效副本 OR 进 RAM BBT + */ +int32_t gd5f_bbt_reload(void); + /* ==================== SPI 原语(FTL 共享) ==================== */ -int gd5f_wait_busy(uint32_t timeout_ms); -int gd5f_write_enable(void); -int gd5f_read_status(uint8_t *p_status); -int gd5f_page_read(uint32_t page_addr); -int gd5f_read_from_cache(uint16_t column, uint8_t *p_buf, size_t size); -int gd5f_program_load(uint16_t column, const uint8_t *p_buf, size_t size); -int gd5f_program_exec(uint32_t page_addr); -int gd5f_block_erase(uint32_t block_addr); -int gd5f_check_ecc(void); +int32_t gd5f_wait_busy(uint32_t timeout_ms); +int32_t gd5f_write_enable(void); +int32_t gd5f_read_status(uint8_t *p_status); +int32_t gd5f_page_read(uint32_t page_addr); +int32_t gd5f_read_from_cache(uint16_t column, uint8_t *p_buf, uint32_t size); +int32_t gd5f_program_load(uint16_t column, const uint8_t *p_buf, uint32_t size); +int32_t gd5f_program_exec(uint32_t page_addr); +int32_t gd5f_block_erase(uint32_t block_addr); +int32_t gd5f_check_ecc(void); #ifdef __cplusplus } diff --git a/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c b/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c index 45897bd..9269d6b 100644 --- a/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c +++ b/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c @@ -200,12 +200,13 @@ DSTATUS disk_initialize(BYTE pdrv) { s_nand.log2_page_size = 11; s_nand.log2_ppb = 6; - s_nand.num_blocks = FTL_NUM_BLOCKS; + /* 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 = 0; + s_cached_lpn = (dhara_sector_t)-1; /* -1 表示缓存未加载;LPN 0 合法,不能用 0 作哨兵 */ s_cache_dirty = 0; { @@ -227,6 +228,21 @@ DSTATUS disk_initialize(BYTE pdrv) { 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; @@ -380,6 +396,7 @@ int nand_ftl_format(void) { } 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; diff --git a/Drivers/BSP/GD5F2GQ5UE/nand_ftl.h b/Drivers/BSP/GD5F2GQ5UE/nand_ftl.h index 84b9363..0735acd 100644 --- a/Drivers/BSP/GD5F2GQ5UE/nand_ftl.h +++ b/Drivers/BSP/GD5F2GQ5UE/nand_ftl.h @@ -9,6 +9,7 @@ extern "C" { int nand_ftl_init(void); int nand_ftl_format(void); +void nand_ftl_deinit(void); #ifdef __cplusplus } diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index 7c442ff..d7cb044 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -2014,6 +2014,16 @@ 1 ..\test\net_test_task.c + + gd5f_test_task.c + 1 + ..\test\gd5f_test_task.c + + + storage_test_task.c + 1 + ..\test\storage_test_task.c + diff --git a/MDK-ARM/build_log.txt b/MDK-ARM/build_log.txt index 01fdea8..f50e611 100644 --- a/MDK-ARM/build_log.txt +++ b/MDK-ARM/build_log.txt @@ -5,68 +5,70 @@ compiling crc.c... compiling ringbuf.c... compiling stm32f4xx_hal_flash_ramfunc.c... compiling stm32f4xx_hal_gpio.c... -compiling stm32f4xx_hal_flash_ex.c... -compiling stm32f4xx_hal_dma.c... -compiling gpio.c... +compiling stm32f4xx_hal_msp.c... +compiling i2c.c... compiling stm32f4xx_hal_dma_ex.c... +compiling stm32f4xx_hal_dma.c... compiling stm32f4xx_hal_timebase_tim.c... compiling stm32f4xx_it.c... -compiling sys_clock.c... -compiling stm32f4xx_hal_rcc.c... -compiling app_main.c... -compiling dma.c... -compiling spi.c... -compiling stm32f4xx_hal_flash.c... -compiling stm32f4xx_hal_msp.c... compiling usart.c... -compiling stm32f4xx_hal_rcc_ex.c... -compiling i2c.c... -compiling adc_task.c... +compiling stm32f4xx_hal_flash.c... +compiling sys_clock.c... +compiling app_main.c... +compiling spi.c... +compiling gpio.c... +compiling dma.c... compiling rs485_task.c... -compiling main.c... -compiling stm32f4xx_hal_tim_ex.c... -compiling net_task.c... +compiling stm32f4xx_hal_rcc_ex.c... +compiling adc_task.c... +compiling stm32f4xx_hal_flash_ex.c... compiling freertos.c... -compiling lftpd_string.c... +compiling stm32f4xx_hal_rcc.c... +compiling main.c... +compiling net_task.c... +compiling stm32f4xx_hal_tim_ex.c... compiling stm32f4xx_hal_tim.c... +compiling lftpd_string.c... compiling croutine.c... -compiling list.c... compiling event_groups.c... -compiling stream_buffer.c... +compiling list.c... compiling queue.c... +compiling stream_buffer.c... compiling timers.c... compiling tasks.c... compiling heap_4.c... compiling port.c... compiling journal.c... -compiling map.c... -compiling ff.c... -compiling stm32f4xx_hal_pwr_ex.c... -compiling stm32f4xx_hal_exti.c... -compiling cmsis_os2.c... compiling stm32f4xx_hal_pwr.c... -compiling system_stm32f4xx.c... +compiling map.c... +compiling stm32f4xx_hal_pwr_ex.c... +compiling ff.c... compiling stm32f4xx_hal_i2c_ex.c... -compiling stm32f4xx_hal_cortex.c... -compiling stm32f4xx_hal_spi.c... compiling stm32f4xx_hal.c... -compiling tpafe5160.c... -compiling rs485.c... +compiling stm32f4xx_hal_cortex.c... +compiling stm32f4xx_hal_exti.c... compiling sd2506.c... -compiling nand_ftl.c... -compiling ch395f.c... -compiling lftpd_io.c... -compiling lftpd_inet.c... +compiling cmsis_os2.c... +compiling stm32f4xx_hal_spi.c... +compiling system_stm32f4xx.c... +compiling rs485.c... compiling gd5f2gq5ue.c... -compiling stm32f4xx_hal_uart.c... +compiling nand_ftl.c... +compiling lftpd_io.c... +compiling tpafe5160.c... +compiling lftpd_inet.c... compiling net_select.c... compiling stm32f4xx_hal_i2c.c... +compiling ch395f.c... compiling lftpd.c... +compiling stm32f4xx_hal_uart.c... compiling net_socket.c... -compiling net_test_task.c... compiling ch395f_test_task.c... +compiling net_test_task.c... +compiling storage_test_task.c... +compiling gd5f_test_task.c... linking... -Program Size: Code=52664 RO-data=1992 RW-data=232 ZI-data=118672 +Program Size: Code=56352 RO-data=3756 RW-data=228 ZI-data=54044 FromELF: creating hex file... ".\STM32F407-Demo\STM32F407-Demo.axf" - 0 Error(s), 0 Warning(s). -Build Time Elapsed: 00:00:17 +Build Time Elapsed: 00:00:16 diff --git a/Src/freertos.c b/Src/freertos.c index 31b4054..9bd7ffc 100644 --- a/Src/freertos.c +++ b/Src/freertos.c @@ -45,30 +45,6 @@ /* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ - -/* 存储测试统计 */ -typedef struct { - uint16_t total; - uint16_t passed; - uint16_t failed; -} storage_test_stats_t; - -#define STORAGE_TEST_CHECK(cond, fmt, ...) do { \ - g_storage_stats.total++; \ - if (cond) { \ - g_storage_stats.passed++; \ - DBG_INFO(" [PASS] " fmt, ##__VA_ARGS__); \ - } else { \ - g_storage_stats.failed++; \ - DBG_ERROR(" [FAIL] " fmt, ##__VA_ARGS__); \ - } \ -} while (0) - -#define STORAGE_TEST_REPORT(name) do { \ - DBG_INFO("=== %s: %d/%d PASSED (failed=%d) ===", \ - name, g_storage_stats.passed, g_storage_stats.total, g_storage_stats.failed); \ -} while (0) - /* USER CODE END PD */ /* Private macro -------------------------------------------------------------*/ @@ -78,8 +54,7 @@ typedef struct { /* Private variables ---------------------------------------------------------*/ /* USER CODE BEGIN Variables */ -static storage_test_stats_t g_storage_stats; -static uint8_t s_perf_buf[4096]; +/* 存储测试变量(g_storage_stats / s_perf_buf)已迁至 test/storage_test_task.c */ /* USER CODE END Variables */ /* Definitions for defaultTask */ osThreadId_t defaultTaskHandle; @@ -162,10 +137,10 @@ void MX_FREERTOS_Init(void) { defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); /* creation of netTask */ - //netTaskHandle = osThreadNew(StartNetTask, NULL, &netTask_attributes); + netTaskHandle = osThreadNew(StartNetTask, NULL, &netTask_attributes); /* creation of ftpTask */ - //ftpTaskHandle = osThreadNew(StartFtpTask, NULL, &ftpTask_attributes); + ftpTaskHandle = osThreadNew(StartFtpTask, NULL, &ftpTask_attributes); /* creation of ch395fTestTask */ ch395fTestTaskHandle = osThreadNew(StartCh395fTestTask, NULL, &ch395fTestTask_attributes); @@ -193,99 +168,8 @@ void MX_FREERTOS_Init(void) { void StartDefaultTask(void *argument) { /* USER CODE BEGIN StartDefaultTask */ - - static FATFS fs; - static FIL fil; - static uint8_t work[FF_MAX_SS]; - char read_buf[64]; - const char *test_str = "Hello FatFS + dhara FTL!"; - - memset(&g_storage_stats, 0, sizeof(g_storage_stats)); - - /* ==================== FTL + FatFS 文件系统测试 ==================== */ - - FRESULT res = f_mount(&fs, "", 1); - if (res == FR_NO_FILESYSTEM) { - MKFS_PARM opts = {FM_FAT32, 0, 0, 0, 0}; - res = f_mkfs("", &opts, work, sizeof(work)); - STORAGE_TEST_CHECK(res == FR_OK, "f_mkfs"); - res = f_mount(&fs, "", 1); - } - STORAGE_TEST_CHECK(res == FR_OK, "f_mount"); - - if (res == FR_OK) { - UINT bw, br; - - res = f_open(&fil, "test.txt", FA_CREATE_ALWAYS | FA_WRITE); - STORAGE_TEST_CHECK(res == FR_OK, "f_open(write)"); - if (res == FR_OK) { - f_write(&fil, test_str, strlen(test_str), &bw); - STORAGE_TEST_CHECK(bw == strlen(test_str), "f_write (%u bytes)", bw); - f_close(&fil); - - res = f_open(&fil, "test.txt", FA_READ); - STORAGE_TEST_CHECK(res == FR_OK, "f_open(read)"); - if (res == FR_OK) { - f_read(&fil, read_buf, sizeof(read_buf) - 1, &br); - f_close(&fil); - read_buf[br] = '\0'; - STORAGE_TEST_CHECK(br == strlen(test_str) && memcmp(test_str, read_buf, br) == 0, - "data verify \"%s\"", read_buf); - } - } - f_unlink("test.txt"); - } - - /* ==================== FTL + FatFS 性能测试 ==================== */ - - DBG_INFO("=== Storage Test: Performance ==="); - - { - FIL pfile; - UINT pw, pr; - uint32_t t0, tw, tr; - uint32_t i; - uint32_t total_kb = 128; - uint32_t chunk = sizeof(s_perf_buf); - uint32_t count = (total_kb * 1024) / chunk; - - memset(s_perf_buf, 0xA5, sizeof(s_perf_buf)); - - /* 写入 */ - t0 = HAL_GetTick(); - if (f_open(&pfile, "perf.dat", FA_CREATE_ALWAYS | FA_WRITE) == FR_OK) { - for (i = 0; i < count; i++) { - f_write(&pfile, s_perf_buf, chunk, &pw); - } - f_close(&pfile); - } - tw = HAL_GetTick() - t0; - - /* 读取 */ - t0 = HAL_GetTick(); - if (f_open(&pfile, "perf.dat", FA_READ) == FR_OK) { - for (i = 0; i < count; i++) { - f_read(&pfile, s_perf_buf, chunk, &pr); - } - f_close(&pfile); - } - tr = HAL_GetTick() - t0; - - f_unlink("perf.dat"); - - if (tw > 0) { - DBG_INFO(" Write: %lu KB in %lu ms = %lu KB/s", - total_kb, tw, (total_kb * 1000) / tw); - } - if (tr > 0) { - DBG_INFO(" Read: %lu KB in %lu ms = %lu KB/s", - total_kb, tr, (total_kb * 1000) / tr); - } - } - - /* ==================== 汇总报告 ==================== */ - - STORAGE_TEST_REPORT("Storage Tests"); + /* 测试任务已全部收归 ch395fTestTask 统一调度,本任务不再承担任何测试 */ + (void)argument; for (;;) { osDelay(1000); diff --git a/docs/GD5F2GQ5UE_Test_Guide.md b/docs/GD5F2GQ5UE_Test_Guide.md new file mode 100644 index 0000000..d1ff06a --- /dev/null +++ b/docs/GD5F2GQ5UE_Test_Guide.md @@ -0,0 +1,741 @@ +# GD5F2GQ5UE NAND Flash 测试规范 + +> 本规范参照 `docs/CH395F_Test_Guide.md` 结构编写,覆盖 `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c` +> 全部公开/私有函数,并向上延伸到 FTL(`nand_ftl.c`)与 FatFS diskio。 +> 详细芯片行为见 `docs/GD5F2GQ5UExxG.md`,已知陷阱见 `docs/GD5F2GQ5UE_Trap_Records.md`。 + +## 1. 概述 + +### 设计原则 + +- **同步 SPI 访问**:GD5F 驱动全部为**同步阻塞** SPI 事务(CS 拉低 → 命令/地址/数据 → CS 拉高), + 由调用任务直接执行,**不经过消息队列**(与 CH395F 的 netTask 串行化模型不同)。 + 因此测试代码可在任意任务上下文调用,但要注意调用任务会被 SPI 传输阻塞数毫秒(擦除最久)。 +- **先擦后写**:NAND 只能将 1 写为 0,**写操作前目标块必须已擦除**,否则数据不可预期(见边界条件 TC-GD5F-1003)。 +- **ECC 默认开启**:`gd5f2gq5ue_init()` 会置 `Feature(B0h).ECC_EN=1`,读/写均走内部 ECC。 +- **坏块以 BBT 管理**:出厂坏块标记在每块的第一页(page 0)spare byte 0(列地址 0x800,见手册 §12.4 / Table 12-6 / Note);`init` 阶段扫描构建 RAM 中的 BBT。 + +### 芯片参数(关键参数) + +| 参数 | 值 | 说明 | +|------|-----|------| +| 制造商/设备 ID | `0xC8` / `0x52` | `gd5f2gq5ue_read_id()` 校验依据 | +| 页数据大小 | 2048 B | `GD5F_PAGE_SIZE` | +| Spare 大小 | 64 B | `GD5F_SPARE_SIZE`(ECC 开启时有效) | +| 单页总长 | 2112 B | `GD5F_TOTAL_PAGE_SIZE`(ECC 开启) | +| 每块页数 | 64 | `GD5F_PAGES_PER_BLOCK` | +| 块大小 | 128 KB | `GD5F_BLOCK_SIZE = 64×2048` | +| 总块数 | 2048 | `GD5F_TOTAL_BLOCKS` | +| 总容量 | 256 MB | `GD5F_TOTAL_SIZE` | +| 最小擦除单位 | 1 块(128 KB) | 不可按页/字节擦除 | +| DMA 阈值 | 32 B | `GD5F_DMA_THRESHOLD`,>32B 走 `HAL_SPI_*_DMA` | + +### 寄存器与状态位 + +| 寄存器 | 地址 | 关键位 | +|--------|------|--------| +| Status | `0xC0` | OIP(b0) 忙标志、WEL(b1) 写使能、E_FAIL(b2)、P_FAIL(b3)、ECCS1/0(b5/b4) ECC 状态 | +| Feature | `0xB0` | ECC_EN(b4)、QE(b0) | +| Protect | `0xA0` | BP2/1/0 块保护;init 后写为 `0x00` 解除保护 | + +ECC 状态(`ECCS1:ECCS0`):`00`=无错;`01/10/11`=纠正 1/2/3 bit;`10`(ECCS1=1,ECCS0=0)= 超出纠正能力(>4bit,不可纠正)。 +驱动 `gd5f_check_ecc()` 在 `(status>>4)&0x03 == 2` 时返回 `GD5F_ECC_ERROR`。 + +### 测试环境 + +- **硬件**:STM32F407ZGTx + GD5F2GQ5UE(SPI NAND) +- **SPI 接口**:SPI1(CubeMX 初始化),引脚: + - CS=`PE0`,SCK=`PB3`,MISO=`PB4`,MOSI=`PB5`,WP=`PB8`,HOLD=`PE1` +- **调试输出**:USART1(115200bps),`[NAND]` 标签(`dbg_log.h`) +- **擦除/编程耗时**(参考驱动超时):PAGE_READ 等待 100ms、PROGRAM_EXEC 等待 1000ms、BLOCK_ERASE 等待 5000ms + +### 统一测试调度(ch395fTestTask) + +所有测试套件(CH395F / 存储 / GD5F)统一收归 `ch395fTestTask` 线程(`test/ch395f_test_task.c` +的 `StartCh395fTestTask`),通过 `test/test_config.h` 中的 `TEST_SUITE_*` 宏在**编译期**选择要跑的套件。 +选中 `TEST_SUITE_GD5F` 时,`test_config.h` 会自动 `#define ENABLE_GD5F_TESTS` 及全部 +`ENABLE_GD5F_PHASE*_TESTS`,随后由 `gd5f_test_run()`(`test/gd5f_test_task.c`)按阶段串行执行。 + +> `defaultTask` **不再承担任何测试**(仅空闲);所有测试都只在 `ch395fTestTask` 中运行。 + +`test/test_config.h` 配置示例(三选一,互斥,多选触发 `#error`): + +```c +// #define TEST_SUITE_CH395F +// #define TEST_SUITE_STORAGE +#define TEST_SUITE_GD5F /* 选中后自动开启 gd5f 全部阶段宏 */ +``` + +执行顺序(由 `gd5f_test_run()` 按阶段串行,与函数依赖一致): +`init/read_id/reset` → `BBT` → `页读写` → `块擦除` → `ECC` → `SPI 原语` → `DMA 边界` → `FTL` → `FatFS` → `边界条件`。 + +### 启用测试 + +1. 在 `test/test_config.h` 取消注释所需的 `TEST_SUITE_*` 宏(如 `TEST_SUITE_GD5F`); +2. 编译(`@build`)确认 0 错误 0 警告; +3. 烧录后串口观察 `[NAND-TEST]` 日志与各 TC 的 `PASS/FAIL`,结尾打印 + `=== GD5F2GQ5UE Tests: X/Y PASSED ===`; +4. 不选任何 `TEST_SUITE_*` 时为正常产品构建(`ch395fTestTask` 空闲,不跑测试)。 + +--- + +## 2. 阶段 1:初始化与 ID 识别 + +### TC-GD5F-101: gd5f2gq5ue_init 全流程 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-101 | +| **优先级** | P0 | +| **类型** | 功能测试 | +| **标题** | 初始化成功(复位→ID 校验→BBT 扫描→ECC 使能→解除块保护) | +| **前置条件** | SPI1 与 GPIO 已由 CubeMX 初始化 | +| **测试步骤** | 1. 调用 `gd5f2gq5ue_init()`
2. 观察日志:Resetting / Reading NAND ID / Scanning bad blocks / Enabling ECC / Unlocking block protection / NAND init OK | +| **通过标准** | 返回 `GD5F_OK`;日志无 `ID mismatch` / `Reset failed`;BBT 扫描打印 bad count | +| **覆盖原则** | 初始化主路径 | + +### TC-GD5F-102: gd5f2gq5ue_read_id 返回正确 ID + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-102 | +| **优先级** | P0 | +| **类型** | 功能测试 | +| **标题** | 读取 MID=0xC8、DID=0x52 | +| **前置条件** | `gd5f2gq5ue_init()` 已成功(或至少 SPI 已初始化) | +| **测试步骤** | 调用 `gd5f2gq5ue_read_id(&mid, &did)` 并比较 | +| **通过标准** | `mid==0xC8 && did==0x52` | +| **覆盖原则** | `gd5f2gq5ue_read_id` | + +### TC-GD5F-103: gd5f2gq5ue_reset 成功 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-103 | +| **优先级** | P1 | +| **类型** | 功能测试 | +| **标题** | 软复位后状态寄存器 OIP 清零 | +| **前置条件** | SPI 已初始化 | +| **测试步骤** | 调用 `gd5f2gq5ue_reset()`;随后 `gd5f_read_status(&s)` 检查 `OIP==0` | +| **通过标准** | `reset` 返回 `GD5F_OK` 且 `s & GD5F_STATUS_OIP == 0` | +| **覆盖原则** | `gd5f2gq5ue_reset` / `gd5f_read_status` | + +--- + +## 3. 阶段 2:坏块管理(BBT) + +> 注:`gd5f2gq5ue_mark_block_bad()` / `gd5f2gq5ue_bbt_clear()` **仅修改 RAM 中的 BBT 位图**, +> 不会写回 Flash 物理标记(与头注释"尝试物理标记"不符,见陷阱节)。重启后由 `init` 重新扫描出厂标记。 + +### TC-GD5F-201: 出厂坏块扫描 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-201 | +| **优先级** | P0 | +| **类型** | 功能测试 | +| **标题** | init 后 BBT 扫描可完成并打印坏块数 | +| **前置条件** | `gd5f2gq5ue_init()` 已成功 | +| **测试步骤** | 观察 init 日志 `BBT scan: N bad blocks found`;调用 `gd5f2gq5ue_print_bbt()` | +| **通过标准** | 扫描完成(N 为合理值,通常 0~数十);`print_bbt` 能列出坏块偏移 | +| **覆盖原则** | BBT 扫描(`gd5f_private_bbt_scan`) | + +### TC-GD5F-202: gd5f2gq5ue_is_block_bad 查询 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-202 | +| **优先级** | P0 | +| **类型** | 功能测试 | +| **标题** | 好块返回 0,坏块返回 1 | +| **前置条件** | `gd5f2gq5ue_init()` 已成功 | +| **测试步骤** | 1. 对已知好块(如 block 0,除非其为坏块)`is_block_bad` 应返回 0
2. 对 `print_bbt` 列出的坏块 `is_block_bad` 应返回 1 | +| **通过标准** | 好块返回 0,坏块返回 1;越界 block 返回 1 | +| **覆盖原则** | `gd5f2gq5ue_is_block_bad` | + +### TC-GD5F-203: mark_block_bad 与 is_block_bad 一致性 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-203 | +| **优先级** | P1 | +| **类型** | 功能测试 | +| **标题** | 标记后查询一致(RAM BBT) | +| **前置条件** | `gd5f2gq5ue_init()` 已成功 | +| **测试步骤** | 取一个好块 B;`gd5f2gq5ue_mark_block_bad(B)` 后 `is_block_bad(B)==1`;再 `gd5f2gq5ue_bbt_clear()` 后 `is_block_bad(B)==0` | +| **通过标准** | mark 后查得 1,clear 后查得 0(RAM 行为符合预期) | +| **覆盖原则** | `gd5f2gq5ue_mark_block_bad` / `gd5f2gq5ue_bbt_clear` | + +### TC-GD5F-204: 初始化 BBT 稳定读取(无需 rescan) + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-204 | +| **优先级** | P2 | +| **类型** | 一致性测试 | +| **标题** | 初始化 BBT 可直接读取且稳定(无需 rescan) | +| **前置条件** | `gd5f2gq5ue_init()` 已成功 | +| **测试步骤** | 记录 `print_bbt` 坏块数;再次 `print_bbt` 比较坏块数(验证 init BBT 可直接读取且稳定) | +| **通过标准** | 两次坏块数一致(出厂标记未变) | +| **覆盖原则** | `gd5f2gq5ue_is_block_bad` / `gd5f2gq5ue_bbt_clear` / `gd5f2gq5ue_mark_block_bad`(init BBT 读取) | + +--- + +## 4. 阶段 3:页读写(跨页) + +> 重要:写操作前目标区域必须已擦除(见阶段 4 / 边界条件 TC-GD5F-1003)。 + +### TC-GD5F-301: 整页写读 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-301 | +| **优先级** | P0 | +| **类型** | 功能测试 | +| **标题** | offset=0, size=2048 写入递增模式并读回比对 | +| **前置条件** | 目标块已擦除 | +| **测试步骤** | 1. 填 `buf[i]=i&0xFF`(或 `(offset+i)&0xFF`)
2. `gd5f2gq5ue_write(0, buf, 2048)`
3. `gd5f2gq5ue_read(0, rbuf, 2048)` 逐字节比对 | +| **通过标准** | 读回与写入完全一致 | +| **覆盖原则** | `gd5f2gq5ue_write` / `gd5f2gq5ue_read`(单页路径) | + +### TC-GD5F-302: 跨页写读 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-302 | +| **优先级** | P0 | +| **类型** | 功能测试 | +| **标题** | offset=1000, size=3000 跨越页边界 | +| **前置条件** | 涉及块已擦除 | +| **测试步骤** | 写入 3000 字节(offset=1000 起),读回同窗口比对 | +| **通过标准** | 跨页数据连续正确(`gd5f2gq5ue_read/write` 自动分页) | +| **覆盖原则** | 跨页拆分逻辑 | + +### TC-GD5F-303: 非对齐 offset 写读 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-303 | +| **优先级** | P1 | +| **类型** | 边界测试 | +| **标题** | offset=1, size=2047 | +| **前置条件** | 目标块已擦除 | +| **测试步骤** | 同 TC-GD5F-301,但 offset=1 | +| **通过标准** | 读回与写入一致(列地址 = offset%2048 正确) | +| **覆盖原则** | 列地址计算 | + +### TC-GD5F-304: 多页顺序写读 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-304 | +| **优先级** | P1 | +| **类型** | 压力测试 | +| **标题** | 连续 10 页(offset=0, size=20480) | +| **前置条件** | 前 10 页所在块已擦除 | +| **测试步骤** | 写入 20480 字节递增模式,读回全量比对 | +| **通过标准** | 全部一致 | +| **覆盖原则** | 多页循环 | + +### TC-GD5F-305: 随机单字节访问 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-305 | +| **优先级** | P2 | +| **类型** | 功能测试 | +| **标题** | 任意 offset,size=1 写入/读回 | +| **前置条件** | 目标块已擦除 | +| **测试步骤** | 在若干随机 offset 写单字节并读回 | +| **通过标准** | 单字节值正确,且不影响同页其余字节(部分页编程约束内) | +| **覆盖原则** | 单字节 `program_load` 列偏移 | + +--- + +## 5. 阶段 4:块擦除 + +### TC-GD5F-401: 单块擦除后读全 0xFF + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-401 | +| **优先级** | P0 | +| **类型** | 功能测试 | +| **标题** | 擦除 block 0(先确认非坏块),读出应全 0xFF | +| **前置条件** | block 0 非坏块 | +| **测试步骤** | `gd5f2gq5ue_erase(0, 128*1024)`;读整块比对全 0xFF | +| **通过标准** | 返回 `GD5F_OK` 且整块读回全 0xFF | +| **覆盖原则** | `gd5f2gq5ue_erase` / `gd5f_block_erase` | + +### TC-GD5F-402: 多块擦除 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-402 | +| **优先级** | P1 | +| **类型** | 功能测试 | +| **标题** | size=3 块(384KB)连续擦除 | +| **前置条件** | 对应块非坏块 | +| **测试步骤** | `gd5f2gq5ue_erase(0, 3*128*1024)`;读回比对全 0xFF | +| **通过标准** | 返回 `GD5F_OK`,范围全 0xFF | +| **覆盖原则** | 多块循环擦除 | + +### TC-GD5F-403: 非块对齐应返回错误 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-403 | +| **优先级** | P0 | +| **类型** | 负向测试 | +| **标题** | offset 或 size 非 128KB 整数倍 | +| **前置条件** | `gd5f2gq5ue_init()` 已成功 | +| **测试步骤** | `gd5f2gq5ue_erase(100, 128*1024)`(offset 不对齐);`gd5f2gq5ue_erase(0, 100)`(size 不对齐) | +| **通过标准** | 两者均返回 `GD5F_ERROR` | +| **覆盖原则** | 对齐校验 | + +### TC-GD5F-404: 擦除-写-读 完整循环 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-404 | +| **优先级** | P0 | +| **类型** | 集成测试 | +| **标题** | erase → write → read 闭环比对 | +| **前置条件** | 目标块非坏块 | +| **测试步骤** | 1. `erase` 目标块
2. `write` 已知模式(如 0xAA 填充/递增)
3. `read` 比对 | +| **通过标准** | 读回 == 写入(验证"先擦后写"链路) | +| **覆盖原则** | 完整 NAND 写流程 | + +--- + +## 6. 阶段 5:ECC 验证 + +### TC-GD5F-501: ECC 开启下写读正确 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-501 | +| **优先级** | P0 | +| **类型** | 功能测试 | +| **标题** | ECC 使能时数据正确,ECC status 无错 | +| **前置条件** | `gd5f2gq5ue_init()`(已使能 ECC);目标块已擦除 | +| **测试步骤** | 写随机/递增模式;`gd5f_page_read(page)` + `gd5f_read_from_cache` 读回;`gd5f_check_ecc()` 检查 | +| **通过标准** | 数据一致且 `gd5f_check_ecc()` 返回 `GD5F_OK`(ECCS=00 无错) | +| **覆盖原则** | ECC 数据路径 | + +### TC-GD5F-502: gd5f_check_ecc 干净读 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-502 | +| **优先级** | P1 | +| **类型** | 功能测试 | +| **标题** | 干净页 `gd5f_check_ecc()` 返回 OK | +| **前置条件** | 已 `gd5f_page_read` 加载页 | +| **测试步骤** | 读后调 `gd5f_check_ecc()` | +| **通过标准** | 返回 `GD5F_OK`(非 `GD5F_ECC_ERROR`) | +| **覆盖原则** | `gd5f_check_ecc` | + +### TC-GD5F-503: ECC 纠错能力(可选) + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-503 | +| **优先级** | P3 | +| **类型** | 可选/深入测试 | +| **标题** | 观察 ECCS 位在纠正场景下的变化 | +| **前置条件** | 已知好块,ECC 开启 | +| **测试步骤** | 写入后读回,检查 `ECCS` 位(00 无错 / 01~11 已纠正 bit);人为制造位翻转较困难,建议仅读状态位验证逻辑 | +| **通过标准** | `gd5f_check_ecc()` 在不可纠正时返回 `GD5F_ECC_ERROR`,否则 `GD5F_OK` | +| **覆盖原则** | ECC 状态位语义 | + +--- + +## 7. 阶段 6:SPI 原语(FTL 共享) + +> 下列函数为 `gd5f2gq5ue.c` 的私有/半公开原语,FTL 直接复用;通过组合调用验证。 + +### TC-GD5F-601: page_read + read_from_cache 组合 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-601 | +| **优先级** | P1 | +| **类型** | 功能测试 | +| **标题** | 单页加载到 cache 并读出 | +| **前置条件** | 目标页有已编程数据 | +| **测试步骤** | `gd5f_page_read(page)` → `gd5f_read_from_cache(0, buf, 2048)` | +| **通过标准** | 数据正确,返回 `GD5F_OK` | +| **覆盖原则** | `gd5f_page_read` / `gd5f_read_from_cache` | + +### TC-GD5F-602: program_load + program_exec 组合 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-602 | +| **优先级** | P1 | +| **类型** | 功能测试 | +| **标题** | 单页编程(load→exec) | +| **前置条件** | 目标块已擦除 | +| **测试步骤** | `gd5f_write_enable()` → `gd5f_program_load(0, buf, 2048)` → `gd5f_program_exec(page)`;读回比对 | +| **通过标准** | 编程后数据一致,无 `P_FAIL` | +| **覆盖原则** | `gd5f_program_load` / `gd5f_program_exec` | + +### TC-GD5F-603: gd5f_block_erase 单块 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-603 | +| **优先级** | P1 | +| **类型** | 功能测试 | +| **标题** | 原语级块擦除(地址=块首页地址) | +| **前置条件** | SPI 已初始化 | +| **测试步骤** | `gd5f_block_erase(block)`(注意传**块编号**,内部转块首页地址,见陷阱 01) | +| **通过标准** | 返回 `GD5F_OK`,无 `E_FAIL` | +| **覆盖原则** | `gd5f_block_erase`(Trap 01 修复点) | + +### TC-GD5F-604: write_enable / read_status(WEL 位) + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-604 | +| **优先级** | P2 | +| **类型** | 功能测试 | +| **标题** | 写使能后 WEL 位置位 | +| **前置条件** | SPI 已初始化 | +| **测试步骤** | `gd5f_write_enable()` → `gd5f_read_status(&s)` 检查 `s & GD5F_STATUS_WEL` | +| **通过标准** | WEL 位为 1 | +| **覆盖原则** | `gd5f_write_enable` / `gd5f_read_status` | + +### TC-GD5F-605: gd5f_wait_busy 正常返回 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-605 | +| **优先级** | P2 | +| **类型** | 功能测试 | +| **标题** | 操作完成后 wait_busy 在超时内返回 0 | +| **前置条件** | 执行一次读/写操作后 | +| **测试步骤** | 调 `gd5f_wait_busy(1000)`,检查返回值 | +| **通过标准** | 返回 `GD5F_OK`(OIP 已清零) | +| **覆盖原则** | `gd5f_wait_busy` | + +--- + +## 8. 阶段 7:DMA 边界(阈值 32 字节) + +> 驱动 `GD5F_DMA_THRESHOLD=32`:`size>32` 走 `HAL_SPI_*_DMA`,否则轮询。两路径结果必须一致。 + +### TC-GD5F-701: 阈值下界(轮询路径) + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-701 | +| **优先级** | P1 | +| **类型** | 边界测试 | +| **标题** | size=32 走非 DMA 路径,读写正确 | +| **前置条件** | 目标块已擦除 | +| **测试步骤** | 写/读 32 字节并比对 | +| **通过标准** | 数据一致 | +| **覆盖原则** | `GD5F_DMA_THRESHOLD` 下界 | + +### TC-GD5F-702: 阈值上界(DMA 路径) + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-702 | +| **优先级** | P1 | +| **类型** | 边界测试 | +| **标题** | size=33 走 DMA 路径,读写正确 | +| **前置条件** | 目标块已擦除 | +| **测试步骤** | 写/读 33 字节并比对 | +| **通过标准** | 数据一致 | +| **覆盖原则** | `GD5F_DMA_THRESHOLD` 上界 | + +### TC-GD5F-703: 大块 DMA 一致性 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-703 | +| **优先级** | P1 | +| **类型** | 功能测试 | +| **标题** | size=2048 整页(DMA 路径)读写正确 | +| **前置条件** | 目标块已擦除 | +| **测试步骤** | 同 TC-GD5F-301,确认 DMA 路径全页正确 | +| **通过标准** | 数据一致 | +| **覆盖原则** | DMA 大数据路径 | + +## 9. 阶段 8:FTL 层(nand_ftl) + +### TC-GD5F-801: nand_ftl_init 成功 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-801 | +| **优先级** | P0 | +| **类型** | 功能测试 | +| **标题** | FTL 初始化(在 driver init 之上建立映射) | +| **前置条件** | `gd5f2gq5ue_init()` 已成功 | +| **测试步骤** | 调用 `nand_ftl_init()` | +| **通过标准** | 返回 0;日志无异常 | +| **覆盖原则** | `nand_ftl_init` | + +### TC-GD5F-802: nand_ftl_format 成功 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-802 | +| **优先级** | P1 | +| **类型** | 功能测试 | +| **标题** | 格式化后可正常使用 | +| **前置条件** | `nand_ftl_init()` 已成功 | +| **测试步骤** | 调用 `nand_ftl_format()` | +| **通过标准** | 返回 0 | +| **覆盖原则** | `nand_ftl_format` | + +--- + +## 10. 阶段 9:FatFS diskio(可选,待 FTL 完成) + +### TC-GD5F-901: 挂载 + 文件写读 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-901 | +| **优先级** | P2 | +| **类型** | 集成测试 | +| **标题** | FatFS 挂载 `/`,写文件后读回比对 | +| **前置条件** | FTL 已 init/format;FatFS diskio 已对接 | +| **测试步骤** | `f_mount` → `f_open` 写若干 KB → `f_close` → `f_open` 读回比对 | +| **通过标准** | 文件内容一致 | +| **覆盖原则** | FatFS diskio 链路 | + +--- + +## 11. 边界条件 + +### TC-GD5F-1001: offset 越界 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-1001 | +| **优先级** | P1 | +| **类型** | 负向测试 | +| **标题** | offset >= TOTAL_SIZE 应返回错误 | +| **前置条件** | `gd5f2gq5ue_init()` 已成功 | +| **测试步骤** | `gd5f2gq5ue_read(GD5F_TOTAL_SIZE, buf, 1)` / `write` 同 | +| **通过标准** | 返回错误码(非 GD5F_OK) | +| **覆盖原则** | 边界保护 | + +### TC-GD5F-1002: size=0 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-1002 | +| **优先级** | P2 | +| **类型** | 负向测试 | +| **标题** | 零长度读写不崩溃 | +| **前置条件** | `gd5f2gq5ue_init()` 已成功 | +| **测试步骤** | `gd5f2gq5ue_read(0, buf, 0)` / `write(0, buf, 0)` | +| **通过标准** | 返回 GD5F_OK 或合理错误,无 HardFault | +| **覆盖原则** | 零长度处理 | + +### TC-GD5F-1003: 未擦除区域写入 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-1003 | +| **优先级** | P0 | +| **类型** | 注意事项/负向 | +| **标题** | 写前未擦除 → 数据不可预期 | +| **前置条件** | 目标块含旧数据(未擦除) | +| **测试步骤** | 直接 `write` 新数据并 `read` 比对 | +| **通过标准** | **不通过比对**(验证 NAND "先擦后写" 特性;测试本身用于确认驱动不会误报成功) | +| **覆盖原则** | NAND 写约束 | + +### TC-GD5F-1004: 写保护解除验证 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-GD5F-1004 | +| **优先级** | P1 | +| **类型** | 功能测试 | +| **标题** | init 后 Protect 寄存器为 0x00,可写 | +| **前置条件** | `gd5f2gq5ue_init()` 已成功 | +| **测试步骤** | `gd5f_private_set_feature` 读 Protect(0xA0);或间接由"可正常 program"推断 | +| **通过标准** | 块保护已解除,编程不返回 `GD5F_PROGRAM_FAIL` | +| **覆盖原则** | 块保护解除 | + +--- + +--- + +## 12. 存储套件测试(TEST_SUITE_STORAGE / storage_test_run) + +> 本套件独立于 GD5F 驱动套件,由 `test_config.h` 的 `TEST_SUITE_STORAGE` 选择, +> 入口 `storage_test_run()`(`test/storage_test_task.c`),统一在 `ch395fTestTask` 中调度。 +> 当前 `storage_test_run()` 已覆盖 mkfs/mount/文件写读/性能(与阶段 9 FatFs 同类路径), +> 并新增下列三项**集成测试**(TC-STO-01~03),覆盖 FTL journal 掉电恢复、大文件/随机访问、坏块注入下的存储可用性。 +> 三项均已于 STORAGE 套件实测通过(`=== Storage Tests: 11/11 PASSED (failed=0) ===`)。 +> +> **已知陷阱(已修复)**:FTL 页缓存用 `s_cached_lpn = 0` 表示"缓存未加载",但 LPN 0 是合法页(FAT 引导扇区所在页)。 +> resume/重新初始化后 `s_cached_lpn` 被复位为 0,导致首次 `f_mount` 读 LBA0 时误判缓存命中、返回 `memset` 的全 0 缓存, +> 引导扇区签名变成 `0000`,FatFs 报 `FR_NO_FILESYSTEM`。正常启动因 `f_mkfs` 兜底分支掩盖了此问题(第二次挂载才真正读盘)。 +> 修复:将"缓存空"哨兵改为非法值 `(dhara_sector_t)-1`(0xFFFFFFFF,远超实际容量),涉及 `disk_initialize` / `nand_ftl_deinit` / `nand_ftl_format`。 + +### TC-STO-01: FTL 掉电恢复(journal 持久化) + +| 字段 | 值 | +|------|-----| +| **ID** | TC-STO-01 | +| **优先级** | P1 | +| **类型** | 集成/可靠性测试 | +| **标题** | 写入文件并同步后,可经 `dhara_map_resume` 恢复映射,文件不丢 | +| **前置条件** | `gd5f2gq5ue_init()` + `nand_ftl_init()` 已成功;FatFS 可挂载 | +| **测试步骤** | 1. `f_mount` → 写已知内容文件 `pl.dat`(4KB 位置相关模式)→ `f_sync`/`f_close` 落盘
2. 模拟掉电:`f_mount("",0)` 卸载 → `nand_ftl_deinit()` 复位 FTL → `nand_ftl_init()` 重新走 `dhara_map_resume`
3. `f_mount("",1)` 重新挂载 → 打开 `pl.dat` 读回比对 | +| **通过标准** | 掉电(模拟)前后文件内容逐字节一致;resume 后映射有效(无异常) | +| **覆盖原则** | `disk_initialize` / `dhara_map_resume` / FatFS 掉电安全 | +| **依赖** | `nand_ftl_deinit()`(清 `s_initialized`/`s_cached_lpn`,使下次 init 真正重新 resume) | +| **实测** | PASS(STORAGE 套件:resume 后 `pl.dat` 内容逐字节一致;修复 `s_cached_lpn` 哨兵 bug 后通过) | + +### TC-STO-02: 大文件 / 多扇区 / 随机 seek + +| 字段 | 值 | +|------|-----| +| **ID** | TC-STO-02 | +| **优先级** | P1 | +| **类型** | 功能/压力测试 | +| **标题** | 256KB 文件顺序写读 + 随机偏移 seek 读一致性 | +| **前置条件** | FTL 已初始化、FatFS 已挂载 | +| **测试步骤** | 1. 写 256KB 文件 `big.dat`,每字节按位置相关模式填充(便于任意偏移校验)
2. 顺序读回全量比对
3. `f_lseek` 到若干随机偏移(如 0/33KB/128KB/200KB/末段)读小块比对 | +| **通过标准** | 顺序与全部随机偏移读回内容均一致(验证 FTL 大范围映射 + GC + FatFS 随机访问) | +| **覆盖原则** | FTL 大范围 LBA 映射 / 垃圾回收 / FatFs `f_lseek` + `f_read` | +| **实测** | PASS(STORAGE 套件:256KB 顺序 + 随机 seek 读回一致) | + +### TC-STO-03: 坏块注入下存储写入 + +| 字段 | 值 | +|------|-----| +| **ID** | TC-STO-03 | +| **优先级** | P1 | +| **类型** | 负向/健壮性测试 | +| **标题** | 运行时注入坏块后,FatFS 仍可正常写读且坏块未被占用 | +| **前置条件** | `gd5f2gq5ue_init()` 成功;FTL 已复位 | +| **测试步骤** | 1. `nand_ftl_format()` 清空旧 map → 选若干出厂好块 `gd5f2gq5ue_mark_block_bad()` 注入(更新并持久化 BBT)
2. `nand_ftl_deinit()` + `nand_ftl_init()` 使 dhara 经 `is_block_bad` 看到新坏块
3. `f_mkfs` 重建文件系统 → 正常文件写读 `bbt.dat` 并校验
4. 校验注入坏块仍 `gd5f2gq5ue_is_block_bad()==1` | +| **通过标准** | 文件内容完整;注入坏块未被 dhara 分配使用(仍报告坏),存储可用 | +| **注意事项** | 本例会**永久**将若干好块标记为坏并持久化(测试设备专用);如需恢复,运行 TC-STO-04(启用 `ENABLE_STORAGE_RECOVERY_TESTS` 编译 STORAGE 套件一次)即可重建 BBT + 重新格式化 | +| **实测** | PASS(STORAGE 套件:注入 3 坏块后文件完整且坏块仍报告坏) | + +### TC-STO-04: 重建 BBT + 重新格式化(恢复) + +| 字段 | 值 | +|------|-----| +| **ID** | TC-STO-04 | +| **优先级** | P1 | +| **类型** | 恢复/清理测试 | +| **标题** | 重建 BBT(重新扫描出厂坏块并持久化)并清空 dhara map,恢复设备干净态 | +| **前置条件** | `gd5f2gq5ue_init()` 成功;需启用 `ENABLE_STORAGE_RECOVERY_TESTS` 编译 | +| **测试步骤** | 1. `gd5f2gq5ue_bbt_rebuild()`(关 ECC → 重新扫描出厂坏块 → 开 ECC → `gd5f_bbt_save` 持久化,覆盖注入坏块)
2. `nand_ftl_format()` 清空 dhara map
3. `gd5f_bbt_dump_flash` 校验持久化 BBT 版本 ≥1 | +| **通过标准** | 重建返回 OK 且持久化 BBT 版本 ≥1;设备坏块回到出厂集合,注入坏块被清除 | +| **注意事项** | 运行一次即恢复;恢复后建议断电重启让 init 重新加载干净 BBT。本 TC 默认关闭,避免常规测试每次重置设备 | +| **依赖** | `gd5f2gq5ue_bbt_rebuild`(详见 §4.8 持久化设计) | + +--- + +## 附录 B:通过/失败汇总 + +| TC-ID | 标题 | 优先级 | 实测 | +|-------|------|--------|------| +| TC-GD5F-101 | init 全流程 | P0 | PASS | +| TC-GD5F-102 | read_id 正确 ID | P0 | PASS | +| TC-GD5F-103 | reset 成功 | P1 | PASS | +| TC-GD5F-201 | 出厂坏块扫描 | P0 | PASS | +| TC-GD5F-202 | is_block_bad 查询 | P0 | PASS | +| TC-GD5F-203 | mark/clear 一致性 | P1 | PASS | +| TC-GD5F-204 | init BBT 稳定(无需 rescan) | P2 | PASS | +| TC-GD5F-205 | 可用块数 < 总块数(保留池生效) | P1 | PASS | +| TC-GD5F-206 | RAM 标记→闪存持久化(bit+version) | P1 | PASS | +| TC-GD5F-207 | 清 RAM→reload 恢复坏块 | P1 | PASS | +| TC-GD5F-301 | 整页写读 | P0 | PASS | +| TC-GD5F-302 | 跨页写读 | P0 | PASS | +| TC-GD5F-303 | 非对齐 offset | P1 | PASS | +| TC-GD5F-304 | 多页顺序写读 | P1 | PASS | +| TC-GD5F-305 | 随机单字节访问 | P2 | PASS | +| TC-GD5F-401 | 单块擦除读 0xFF | P0 | PASS | +| TC-GD5F-402 | 多块擦除 | P1 | PASS | +| TC-GD5F-403 | 非块对齐返回错误 | P0 | PASS | +| TC-GD5F-404 | 擦-写-读 闭环 | P0 | PASS | +| TC-GD5F-501 | ECC 开启写读 | P0 | PASS | +| TC-GD5F-502 | check_ecc 干净读 | P1 | PASS | +| TC-GD5F-503 | ECC 纠错(可选) | P3 | 未实现(可选) | +| TC-GD5F-601 | page_read+cache 组合 | P1 | PASS | +| TC-GD5F-602 | program_load+exec 组合 | P1 | PASS | +| TC-GD5F-603 | block_erase 原语 | P1 | PASS | +| TC-GD5F-604 | write_enable WEL 位 | P2 | PASS | +| TC-GD5F-605 | wait_busy 正常返回 | P2 | PASS | +| TC-GD5F-701 | DMA 阈值下界(32) | P1 | PASS | +| TC-GD5F-702 | DMA 阈值上界(33) | P1 | PASS | +| TC-GD5F-703 | 大块 DMA 一致性 | P1 | PASS | +| TC-GD5F-801 | nand_ftl_init | P0 | PASS | +| TC-GD5F-802 | nand_ftl_format | P1 | PASS | +| TC-GD5F-901 | FatFS 挂载写读 | P2 | PASS | +| TC-GD5F-1001 | offset 越界 | P1 | PASS | +| TC-GD5F-1002 | size=0 | P2 | PASS | +| TC-GD5F-1003 | 未擦除写入 | P0 | PASS | +| TC-GD5F-1004 | 写保护解除 | P1 | PASS | +| TC-STO-01 | FTL 掉电恢复(journal) | P1 | PASS | +| TC-STO-02 | 大文件/随机 seek | P1 | PASS | +| TC-STO-03 | 坏块注入下存储写入 | P1 | PASS | +| TC-STO-04 | BBT 重建 + 重新格式化(恢复) | P1 | 受 `ENABLE_STORAGE_RECOVERY_TESTS` 控制(默认关) | + +> 当前实测:`TEST_SUITE_GD5F` 构建一次运行全部阶段,结尾输出 `=== GD5F2GQ5UE Tests: 65/65 PASSED (failed=0) ===`(65 为各 TC 内部断言总数,含 TC-GD5F-205~207 BBT 持久化用例;TC 覆盖见上表)。`TEST_SUITE_STORAGE` 套件结尾输出 `=== Storage Tests: 11/11 PASSED (failed=0) ===`(TC-STO-01/02/03 各含若干内部断言,且 TC-STO-01 依赖的 `s_cached_lpn` 哨兵 bug 已修复)。两套件均 0 错误 0 警告通过 MDK 编译。 + +--- + +## 附录 C:驱动函数覆盖 + +| 函数 | 覆盖 TC | +|------|---------| +| `gd5f2gq5ue_init` | TC-GD5F-101 | +| `gd5f2gq5ue_read_id` | TC-GD5F-102 | +| `gd5f2gq5ue_reset` | TC-GD5F-103 | +| `gd5f2gq5ue_read` | TC-GD5F-301/302/303/304/305 | +| `gd5f2gq5ue_write` | TC-GD5F-301/302/303/304/305 | +| `gd5f2gq5ue_erase` | TC-GD5F-401/402/403/404 | +| `gd5f2gq5ue_is_block_bad` | TC-GD5F-202 | +| `gd5f2gq5ue_mark_block_bad` | TC-GD5F-203 | +| `gd5f2gq5ue_bbt_clear` | TC-GD5F-203 | +| `gd5f2gq5ue_is_block_bad` / 初始化 BBT | TC-GD5F-204 | +| `gd5f2gq5ue_print_bbt` | TC-GD5F-201 | +| `gd5f_wait_busy` | TC-GD5F-605 | +| `gd5f_write_enable` | TC-GD5F-604 | +| `gd5f_read_status` | TC-GD5F-103/604 | +| `gd5f_page_read` | TC-GD5F-601 | +| `gd5f_read_from_cache` | TC-GD5F-601 | +| `gd5f_program_load` | TC-GD5F-602 | +| `gd5f_program_exec` | TC-GD5F-602 | +| `gd5f_block_erase` | TC-GD5F-603 | +| `gd5f_check_ecc` | TC-GD5F-501/502 | +| `nand_ftl_init` | TC-GD5F-801 | +| `nand_ftl_format` | TC-GD5F-802 | +| `nand_ftl_deinit` | TC-STO-01 / TC-STO-03 | +| `gd5f2gq5ue_bbt_rebuild` | TC-STO-04 | +| `storage_test_run`(存储套件) | TC-STO-01/02/03/04 | + +--- + +## 附录 D:参数速查 + +| 项 | 值 | +|----|-----| +| 页数据/Spare/总长 | 2048 / 64 / 2112 B | +| 每块页数 / 块大小 | 64 / 128 KB | +| 总块数 / 容量 | 2048 / 256 MB | +| ID | MID=0xC8, DID=0x52 | +| 擦除最小单位 | 1 块(128 KB,须对齐) | +| DMA 阈值 | 32 B(>32 走 DMA) | +| 状态寄存器 | 0xC0;OIP=0, WEL=1, E_FAIL=2, P_FAIL=3, ECCS=4/5 | +| Feature 寄存器 | 0xB0;ECC_EN=4, QE=0 | +| Protect 寄存器 | 0xA0;init 后写 0x00 解除保护 | +| SPI 引脚 | CS=PE0, SCK=PB3, MISO=PB4, MOSI=PB5, WP=PB8, HOLD=PE1 | + diff --git a/docs/GD5F2GQ5UE_Trap_Records.md b/docs/GD5F2GQ5UE_Trap_Records.md index 5c6b587..2683678 100644 --- a/docs/GD5F2GQ5UE_Trap_Records.md +++ b/docs/GD5F2GQ5UE_Trap_Records.md @@ -29,3 +29,107 @@ - D8h 和 13h/10h 都使用 24-bit 行地址,格式一致 - 块擦除不使用 `block × block_size`(字节偏移),而用块首页地址 - 芯片手册的 memory mapping 必须严格遵守 + +## Trap 02 — 测试缓冲越界导致跨页比对误判(有效) + +**发现时间**:2026-08-26 + +**现象**: +`gd5f_test_task.c` 中 `s_wbuf` / `s_rbuf` 按单页大小定义: +```c +static uint8_t s_wbuf[GD5F_PAGE_SIZE]; /* 2048 字节 */ +static uint8_t s_rbuf[GD5F_PAGE_SIZE]; /* 2048 字节 */ +``` +TC-GD5F-302 跨页写读用 `gd5f2gq5ue_write(base + 1000, s_wbuf, 3000)` / +`read(..., s_rbuf, 3000)`。驱动实际把 3000 字节正确写入了 NAND,但测试缓冲只有 +2048 字节,发生**数组越界**: +- 前 2048 字节写入合法区(page0 全 + page1 前 1000 字节)→ 比对正确; +- page1 剩余 952 字节被写到 `s_rbuf[2048..2999]`(越界),落入相邻内存,**丢失**; +- 比对时读 `s_rbuf[2048..2999]` 为越界垃圾 → 误判 `cross-page MISMATCH`。 + +**根因**: +测试缓冲未覆盖实际最大读写长度(跨页场景超过单页 2048 字节)。 + +**修复**: +将缓冲放大到 ≥ 最大读写长度(如 `GD5F_PAGE_SIZE * 2`,即 4096): +```c +static uint8_t s_wbuf[GD5F_PAGE_SIZE * 2]; +static uint8_t s_rbuf[GD5F_PAGE_SIZE * 2]; +``` + +**验证结果**: +缓冲放大后 TC-GD5F-302 一次通过,不再误判。 + +**经验教训**: +- 任何“跨页 / 大于单页”的读写测试,缓冲必须按**实际长度上限**而非单页大小分配 +- 越界写会污染相邻全局变量,可能引起其它用例莫名失败,定位时优先怀疑缓冲尺寸 + +## Trap 03 — 驱动读写擦缺越界检查(有效 / 健壮性缺口) + +**发现时间**:2026-08-26 + +**现象**: +TC-GD5F-1001 用 `offset = GD5F_TOTAL_SIZE`(恰好超出末尾 1 字节)做 read/write, +期望驱动拒绝(返回错误),但 `gd5f2gq5ue_read` / `write` / `erase` 原实现**无任何边界校验**, +直接计算 `page_addr = offset / PAGE_SIZE` 后继续操作,返回 `GD5F_OK` → 测试 FAIL。 +同时这也意味着正常调用若传入越界参数,会静默访问到回绕后的非法页。 + +**根因**: +`gd5f2gq5ue_read` / `write` / `erase` 入口未校验 `offset` / `size` 是否在 +`[0, GD5F_TOTAL_SIZE]` 容量范围内。 + +**修复**: +在三个函数入口统一加边界检查(FTL/diskio 均在容量内访问,不受影响): +```c +if (offset < 0 || (unsigned long)offset + size > (unsigned long)GD5F_TOTAL_SIZE) { + return GD5F_ERROR; +} +``` +`erase` 在原有的块对齐检查之后、循环之前加同一判断即可。 + +**验证结果**: +- TC-GD5F-1001 read/write 越界均被拒绝(ret=-1),测试 PASS +- TC-GD5F-1002 size=0 仍返回 `GD5F_OK`(offset 合法且 size=0 不越界),符合预期 +- 该检查还顺带逮到 Trap 04 的测试越界擦除(见下) + +**经验教训**: +- 面向字节偏移的裸 NAND 接口必须做容量边界检查,调用方传错参数时尽早失败 +- 无越界检查的驱动容易“静默回绕”,问题极难定位 + +## Trap 04 — gd5f_find_run 返回语义与多块擦除窗口错位(测试 bug) + +**发现时间**:2026-08-26 + +**现象**: +TC-GD5F-402 多块擦除(`gd5f_find_run(3)` 后擦 3 块)返回 `ret=-1`。 +经 Trap 03 的边界检查定位:实际要擦的块超出了设备末尾。 + +**根因**: +`gd5f_find_run(n)` 原实现返回的是**连续好块的末尾块号** `top` +(校验 `top, top-1, ..., top-(n-1)` 共 n 块),但调用方把返回值当作**起始块号**使用: +```c +base3 = (long)blk3 * GD5F_BLOCK_SIZE; +gd5f2gq5ue_erase(base3, 3 * GD5F_BLOCK_SIZE); /* 实际擦 blk3, blk3+1, blk3+2 */ +``` +当 `gd5f_find_run(3)` 返回的末尾块贴到设备末尾(例如 2047,即块 2045/2046/2047 为好块) +时,调用方实际要擦 `2047, 2048, 2049`,块 2048/2049 越界。 +`n=1` 时首尾块号相同所以不暴露,仅 `n>1` 暴露。 + +**修复**: +统一让 `gd5f_find_run` 返回**起始块号**,与 `n=1` 的用法一致,并仍避开块 0: +```c +if (ok) { + return top - (uint32_t)(n - 1); /* 返回连续好块的起始块号 */ +} +``` +循环条件保持 `top >= (uint32_t)(n - 1) + 1`,保证起始块 ≥ 1。 + +**验证结果**: +修复后 TC-GD5F-402 多块擦除(ret=0)及“擦后全 0xFF”均 PASS; +全套用例最终 60/60 PASSED。 + +**经验教训**: +- 返回“连续区间”的辅助函数,其返回值是起点还是终点必须在注释/命名中明确, + 调用方与实现必须一致 +- 这类错位 bug 往往只在“区间贴到设备边界”时才触发,正常情况能过,建议测试覆盖边界块 + diff --git a/docs/STM32F4-Base存储架构说明.md b/docs/STM32F4-Base存储架构说明.md index 67d794d..8446f20 100644 --- a/docs/STM32F4-Base存储架构说明.md +++ b/docs/STM32F4-Base存储架构说明.md @@ -142,7 +142,7 @@ HAL_Init → SystemClock_Config → MX_GPIO_Init → MX_USART1_UART_Init 2. READ_ID (9Fh) └─ 校验 MID=0xC8, DID=0x52 3. BBT 扫描 - └─ 读每块最后一页 (page 63) spare byte 0 + └─ 读每块第一页 (page 0) spare byte 0(列地址 0x800,手册 §12.4 初始坏块标记位) └─ 非 0xFF 即出厂坏块,写入 s_bbt[] 4. SET_FEATURE (B0h=10h) — 使能内部 8bit ECC 5. SET_FEATURE (A0h=00h) — 解除全部块保护 @@ -203,18 +203,111 @@ DMA 消除了轮询模式下 SPI 状态寄存器查检的逐字节 CPU 开销, ### 4.6 BBT (Bad Block Table) -- 初始化时扫描全部 2048 块最后一页的 spare byte 0 +- 初始化时扫描全部 2048 块第一页(page 0)的 spare byte 0(列地址 0x800) - `s_bbt[256]` 位图数组,1 bit 标识 1 个块 (0=好, 1=坏) - `gd5f2gq5ue_is_block_bad(block)` — 查询坏块状态 - `gd5f2gq5ue_mark_block_bad(block)` — 标记坏块 (FTL 层在擦除/编程失败时调用) - `gd5f2gq5ue_bbt_clear()` — 清空 BBT(慎用,仅在重建时使用) -- `gd5f2gq5ue_bbt_rescan()` — 重新扫描并重建 BBT(需 ECC 禁用时调用,见 GD5F2GQ5UE_Trap_Records.md) +- `gd5f_bbt_scan()` — 在 `gd5f2gq5ue_init()` 阶段、ECC 使能前扫描构建 BBT;运行时不再提供公开重扫接口(见 GD5F2GQ5UE_Trap_Records.md) - `gd5f2gq5ue_print_bbt()` — 打印 BBT 摘要(调试用) -> **注意**:BBT 只能在 ECC 禁用时扫描(`gd5f_bbt_scan()`),ECC 使能后 spare area 会被 ECC 引擎干扰导致坏块检测不可靠。运行时坏块只来源于真实擦除/编程失败,不要主动重新扫描。 +> **注意**:`gd5f_private_bbt_scan()` 选择在 ECC 使能前扫描。按手册 §12.6,ECC 使能后整段 spare 仍可读取(仅 0x840~0x87F 的 ECC 校验区禁止编程),因此读取 0x800(spare byte 0)即便 ECC 开启也安全;但扫描放在 ECC 禁用阶段是更稳妥的默认做法。运行时坏块只来源于真实擦除/编程失败,不要主动重新扫描。 + +### 4.7 页内寻址模型(行地址 / 列地址) + +NAND 访问是**二维寻址**,两个地址含义不同: + +| 维度 | 含义 | 由谁发出 | 决定什么 | +|------|------|----------|----------| +| 行地址(页地址 / page address) | 第几页 | `gd5f_page_read(page)` | 把哪一页从存储阵列搬进芯片内部 cache 寄存器 | +| 列地址(column address) | 该页内第几个字节 | `gd5f_read_from_cache(column, ...)`(发送 `0Bh` + 2 字节列地址) | 从 cache 里偏移 `column` 处开始吐数据 | + +**一页的内部布局**(手册 §12.6 Table 12-8):一页 = 2KB 主数据 + 64B 备用区(spare) = 2112 字节,页内连续编址为 column 0 ~ 2111: + +``` + page (2112B) = main(2048B) + spare(64B) + column: 0 2047 2048 2111 + ↑ + spare[0] / 坏块标记位 (0x800) +``` + +- `column 0 ~ 2047` → 主数据区(main data) +- `column 2048 ~ 2111` → 备用区(spare),其中 **2048(0x800)就是 spare 的第 0 字节** + +**为什么坏块标记的列地址是 2048**:main 区占据了前 2048 字节,spare 区从 2048 起算,故 spare byte 0 落在列地址 2048(0x800)。驱动把 `GD5F_PAGE_SIZE`(=2048)作为 `column` 传给 `gd5f_read_from_cache()`(`gd5f2gq5ue.c:307-308` 将其拼成 2 字节列地址),芯片即从 spare[0] 开始回数据——这正是出厂坏块标记位(手册 §12.4 / Table 12-6:First spare area location = Byte 2048)。 + +> 一句话:**page 选页,column 选页内字节偏移;main 占前 2048 字节,spare 从 0x800 起算。** + +### 4.8 BBT 持久化存储(2026-08-27 新增) + +**需求背景**:出厂坏块标记写在每块首页 `spare[0]`(page 0),但 NAND 的**块擦除会同时清空主区与 spare 区**,因此运行时坏块(擦除/编程失败产生)无法回写到原厂标记位——否则该块一旦被擦除,坏块标记即丢失。必须把运行期坏块信息存到独立的、不参与擦写的**保留区域**,并在每次上电时加载回来。 + +**保留块池(BBT Pool)**: + +- 从 NAND 顶部向下挑选 `GD5F_BBT_POOL_COUNT`(=4)个**出厂好块**作为 BBT 保留池,不交给 dhara 管理。 +- `gd5f_private_bbt_locate_pool()` 在 init 阶段从 `GD5F_TOTAL_BLOCKS-1` 向下收集前 4 个 `s_bbt[b]==0`(出厂好)的块号,存入 `s_bbt_pool_blocks[]`,并把 `s_usable_blocks` 设为其中**最低的块号**。 +- dhara 看到的可用块数 = `gd5f_get_usable_blocks()` = `s_usable_blocks`,即保留池以下的块;整个保留池(顶部 4 块)对文件系统不可见。文件系统可用块数由 2048 降为约 2044(若顶部块含出厂坏块则略少)。 + +**闪存上的 BBT 记录格式**(每个保留块 page 0): + +``` +偏移 长度 字段 +0 4 magic = "GBBT" +4 4 version (uint32 LE, 单调递增) +8 4 len (uint32 LE, = GD5F_BBT_SIZE = 256) +12 4 crc32 (uint32, 对下方 bitmap 计算, 多项式 0xEDB88320) +16 256 bitmap (GD5F_BBT_SIZE 字节, 1 bit / 块, 与 RAM s_bbt 同布局) +──────────────────────────────────── +合计 272 字节 (GD5F_BBT_HDR_SIZE + GD5F_BBT_SIZE) +``` + +**写入(掉电安全,轮转)— `gd5f_bbt_save()`**: + +- 版本号自增:`ver = s_bbt_version + 1` +- 轮转写入下一保留槽 `s_bbt_write_slot`(环形指针,遇损坏槽 `s_bbt_slot_dead` 跳过) +- 先 `BLOCK_ERASE` 目标保留块,再 `PROGRAM_LOAD(0)` 写入整页(header+bitmap),最后 `PROGRAM_EXEC` +- 若某保留块自身擦除/编程失败,置 `s_bbt_slot_dead` 对应位并跳到下一槽 +- 每次 `gd5f2gq5ue_mark_block_bad()` 标记新坏块后都会调用 `gd5f_bbt_save()`;写入极罕见(仅坏块事件触发),磨损可忽略 + +**加载 — `gd5f_bbt_load()` → `gd5f_private_bbt_find_best()`**: + +- 遍历全部保留块,校验 `magic` / `len` / `crc32`,丢弃损坏副本 +- 选出**版本号最高**的有效副本,将其 bitmap `OR` 进 RAM `s_bbt`(RAM 此时已含出厂坏块,`gd5f_bbt_scan` 先于本步执行) +- 记录 `s_bbt_version` 与下一轮转槽 `s_bbt_write_slot` +- 掉电保护:每次写入都是"擦除 + 整页编程"的完整新副本;若写入中途掉电,旧的高版本有效副本仍在,上电时按最高版本选取,不会读到半成品 + +**上电初始化顺序(与 §4.3 对应补充)**: + +``` +1. RESET → READ_ID → 校验 MID/DID +2. gd5f_bbt_scan() // 出厂坏块扫描 (ECC 关闭), 填充 s_bbt +3. SET_FEATURE(0xB0,0x10) // 使能内部 8bit ECC +4. SET_FEATURE(保护,0x00) // 解除块保护 +5. gd5f_private_bbt_locate_pool() // 选保留池, 设定 s_usable_blocks +6. gd5f_bbt_load() // 叠加持久化的运行期坏块 +``` + +**调试 / 测试 API**: + +| 函数 | 说明 | +|------|------| +| `gd5f_bbt_dump_flash(p_bbt, p_version)` | 读取闪存中当前生效的 BBT 位图与版本(验证持久化用) | +| `gd5f_bbt_wipe_pool()` | 擦除保留块池(撤销持久化,回到仅出厂扫描态;出厂重置/测试用) | +| `gd5f_bbt_reload()` | 从保留池重新加载持久化 BBT 到 RAM | + +**关键常量**: + +| 宏 / 变量 | 位置 | 值 | 说明 | +|-----------|------|----|------| +| `GD5F_BBT_POOL_COUNT` | `gd5f2gq5ue.h` | 4 | 保留块数量 | +| `GD5F_BBT_SIZE` | `gd5f2gq5ue.c` | 256 (`=TOTAL_BLOCKS/8`) | 位图字节数(覆盖 2048 块,每块 1 bit) | +| `GD5F_BBT_HDR_SIZE` | `gd5f2gq5ue.c` | 16 | 头部长度(magic+ver+len+crc) | +| `s_bbt_magic` | `gd5f2gq5ue.c` | `"GBBT"` | BBT 存储魔数 | --- + + ## 5. dhara FTL (Flash Translation Layer) ### 5.1 概述 @@ -371,7 +464,7 @@ f_mkfs("", &opts, work, size) | `gd5f2gq5ue_is_block_bad(block)` | 查询坏块 | | `gd5f2gq5ue_mark_block_bad(block)` | 标记坏块 | | `gd5f2gq5ue_bbt_clear()` | 清空 BBT | -| `gd5f2gq5ue_bbt_rescan()` | 重新扫描 BBT | +| `gd5f_bbt_scan()` | init 时扫描构建 BBT(内部静态) | | `gd5f2gq5ue_print_bbt()` | 打印 BBT | **SPI 原语(FTL 和测试共享):** @@ -460,6 +553,6 @@ if (nand_ftl_format() == 0) { 1. **擦除对齐** — `gd5f2gq5ue_erase()` 的 offset 和 size 必须严格按 GD5F_BLOCK_SIZE (128KB) 对齐和整数倍。 2. **写前擦除** — NAND 不能原地覆写,FTL 内部自动管理擦除,但直接调用 `gd5f2gq5ue_write()` 前必须确保目标块已擦除。 3. **FTL 首个扇区** — `dhara_map_init` 的第 5 个参数 (journal 页数) 影响 GC 效率,当前为 4,增大可减少写入放大但占用更多内存。 -4. **坏块传播** — FTL 在擦除/编程失败后自动调用 `dhara_nand_mark_bad` → `gd5f2gq5ue_mark_block_bad`,BBT 在 RAM 中更新,下次复位后重新扫描出厂坏块并叠加运行时坏块。 +4. **坏块传播** — FTL 在擦除/编程失败后自动调用 `dhara_nand_mark_bad` → `gd5f2gq5ue_mark_block_bad`,BBT 在 RAM 中更新并立即通过 `gd5f_bbt_save()` 持久化到保留块池;下次复位后 `gd5f_bbt_scan()` 重建出厂坏块,`gd5f_bbt_load()` 再叠加保留池中保存的运行期坏块,因此运行期坏块在掉电后依然有效(详见 §4.8)。 5. **功耗** — 擦除操作最大耗时约 5ms (驱动超时设为 5s),页编程约 600ms (超时 1s),读写操作快。在低功耗场景需注意合理安排操作时序。 6. **缓存一致性** — 单页缓存 (s_cache_buf) 仅对 FatFS 层可见,多任务读写同一文件需在应用层同步。 diff --git a/test/ch395f_test.h b/test/ch395f_test.h index c579ae2..e1b789f 100644 --- a/test/ch395f_test.h +++ b/test/ch395f_test.h @@ -54,7 +54,7 @@ extern "C" { /* ---- NET 层(阶段 6~11)---- */ //#define ENABLE_PHASE6_TESTS /* [网] TCP Echo(单连接基础收发) */ //#define ENABLE_PHASE7_TESTS /* [网] TCP Client 主动连接(回显收发 + 关闭重连 + 连接超时) */ -#define ENABLE_PHASE8_TESTS /* [网] 大文件传输(100KB 文件收发) */ +//#define ENABLE_PHASE8_TESTS /* [网] 大文件传输(100KB 文件收发) */ //#define ENABLE_PHASE9_TESTS /* [网] Select/Poll I/O 多路复用 */ //#define ENABLE_PHASE10_TESTS /* [网] 边界条件测试 */ //#define ENABLE_PHASE11_TESTS /* [网] KeepAlive 超时断开测试 */ diff --git a/test/ch395f_test_task.c b/test/ch395f_test_task.c index bdde144..17d13f6 100644 --- a/test/ch395f_test_task.c +++ b/test/ch395f_test_task.c @@ -17,6 +17,9 @@ #include "net_socket.h" #include "ch395f_test.h" #include "ch395f.h" +#include "test_config.h" +#include "storage_test.h" +#include "gd5f_test.h" #define DBG_TAG "[TEST_TASK]" #include "dbg_log.h" @@ -48,7 +51,7 @@ test_stats_t g_test_stats; static void test_parse_ip(const char *str, uint8_t *ip_arr) { uint32_t val; - int i; + int32_t i; for (i = 0; i < 4; i++) { val = 0; @@ -121,7 +124,7 @@ static void phase1_run(void) { ch395f_set_ip_addr(set_ip); memset(ip_info, 0, sizeof(ip_info)); ch395f_get_ip_inf(ip_info); - int match = (ip_info[0] == 192 && ip_info[1] == 168 && + int32_t match = (ip_info[0] == 192 && ip_info[1] == 168 && ip_info[2] == 1 && ip_info[3] == 100); DBG_INFO(" [P1-03] ch395f_set_ip_addr(192.168.1.100)"); DBG_INFO(" expect: IP[0..3] = 192.168.1.100"); @@ -141,7 +144,7 @@ static void phase1_run(void) { ch395f_set_gwip_addr(set_gw); memset(ip_info, 0, sizeof(ip_info)); ch395f_get_ip_inf(ip_info); - int match = (ip_info[4] == 192 && ip_info[5] == 168 && + int32_t match = (ip_info[4] == 192 && ip_info[5] == 168 && ip_info[6] == 1 && ip_info[7] == 1); DBG_INFO(" [P1-04] ch395f_set_gwip_addr(192.168.1.1)"); DBG_INFO(" expect: GW[0..3] = 192.168.1.1"); @@ -161,7 +164,7 @@ static void phase1_run(void) { ch395f_set_mask_addr(set_mask); memset(ip_info, 0, sizeof(ip_info)); ch395f_get_ip_inf(ip_info); - int match = (ip_info[8] == 255 && ip_info[9] == 255 && + int32_t match = (ip_info[8] == 255 && ip_info[9] == 255 && ip_info[10] == 255 && ip_info[11] == 0); DBG_INFO(" [P1-05] ch395f_set_mask_addr(255.255.255.0)"); DBG_INFO(" expect: MASK[0..3] = 255.255.255.0"); @@ -503,8 +506,8 @@ static void phase2_tc202_once(uint8_t sock, uint16_t local_port) uint16_t n; uint16_t rl; uint32_t tick; - int k; - int match; + int32_t k; + int32_t match; for (k = 0; k < 64; k++) { tx[k] = (uint8_t)k; @@ -596,7 +599,7 @@ static void phase2_tc202(void) { uint8_t sock = 0; uint8_t ip_arr[4]; - int i; + int32_t i; test_parse_ip(PHASE2_REMOTE_IP, ip_arr); @@ -804,7 +807,7 @@ static void phase2_run(void) static void phase3_run(void) { uint8_t sock = 0; uint8_t rx_buf[300] = {0}; - int counted; + int32_t counted; DBG_INFO("=== CH395F Phase 3 Tests (UDP) ==="); @@ -845,8 +848,8 @@ static void phase3_run(void) { counted = 0; while (counted < PHASE3_TOTAL_ROUNDS) { uint32_t tick = HAL_GetTick(); - int echo_ok = 0; - int is_probe = 0; + int32_t echo_ok = 0; + int32_t is_probe = 0; while (HAL_GetTick() - tick < 30000) { uint16_t rlen = ch395f_get_recv_len(sock); if (rlen > 8) { @@ -925,7 +928,7 @@ static void phase3_run(void) { static void phase4_run(void) { uint8_t ip_info[20] = {0}; - int same = 0; + int32_t same = 0; DBG_INFO("=== CH395F Phase 4 Tests (DHCP) ==="); @@ -1015,7 +1018,7 @@ static void phase4_run(void) { ch395f_set_des_ip(anno_sock, bcast); ch395f_set_des_port(anno_sock, 0); - int hello_ok = 0; + int32_t hello_ok = 0; uint32_t wait = HAL_GetTick(); while (HAL_GetTick() - wait < PHASE4_HELLO_WAIT_MS) { ch395f_get_glob_int_status_all(); @@ -1112,13 +1115,13 @@ static void phase5_run(void) { static uint8_t exp[PHASE5_CHUNK_SIZE]; uint8_t sock = 0; uint8_t ip_arr[4]; - int chunks; - int bad = 0; - int conn_ok = 0; - int blk_tx = 0; - int blk_rx = 0; - int can_write = 1; - int progress_64k = -1; + int32_t chunks; + int32_t bad = 0; + int32_t conn_ok = 0; + int32_t blk_tx = 0; + int32_t blk_rx = 0; + int32_t can_write = 1; + int32_t progress_64k = -1; uint32_t send_bytes; uint32_t sum_tx = 0; uint32_t sum_rx = 0; @@ -1345,8 +1348,9 @@ void StartCh395fTestTask(void *argument) { (void)argument; - DBG_INFO("Test task started"); + DBG_INFO("Test task started, suite=%s", TEST_SUITE_NAME); +#if defined(TEST_SUITE_CH395F) #if defined(ENABLE_HW_LAYER_TESTS) /* 硬件层模式(阶段 1~5):netTask 未创建,由本任务完成一次性协议栈 * 初始化并独占 CH395F —— 无 net_poll() 并发读取中断,无 SPI 并发事务 */ @@ -1384,8 +1388,13 @@ void StartCh395fTestTask(void *argument) #if defined(ENABLE_PHASE7_TESTS) net_layer_client_test_main(); #else - net_layer_test_main(); + net_layer_test_main(); #endif +#endif /* TEST_SUITE_CH395F */ +#elif defined(TEST_SUITE_STORAGE) + storage_test_run(); +#elif defined(TEST_SUITE_GD5F) + gd5f_test_run(); #endif for (;;) { diff --git a/test/gd5f_test.h b/test/gd5f_test.h new file mode 100644 index 0000000..eba7544 --- /dev/null +++ b/test/gd5f_test.h @@ -0,0 +1,89 @@ +/* + * 模块名称:GD5F2GQ5UE Driver Test Suite + * 模块功能:GD5F2GQ5UE SPI NAND Flash 驱动测试入口,按阶段组织测试用例, + * 覆盖 gd5f2gq5ue.c 全部公开/私有函数,并延伸到 FTL(nand_ftl)与 + * FatFS diskio。详见 docs/GD5F2GQ5UE_Test_Guide.md。 + * 适用平台:STM32F4 系列(GD5F2GQ5UE SPI NAND) + * 作者:王建锋 + * 创建日期:2026-08-26 + * + * === 测试阶段一览(与指南 TC 一一对应)=== + * Phase | 描述 + * ---------|----------------------------------------------- + * Phase 1 | 初始化与 ID 识别(init / read_id / reset) + * Phase 2 | 坏块管理(BBT:scan / is_block_bad / mark+clear / rescan) + * Phase 3 | 页读写(跨页 / 非对齐 / 多页 / 单字节) + * Phase 4 | 块擦除(单块 / 多块 / 非对齐负向 / 擦写读闭环) + * Phase 5 | ECC 验证(开启下写读 / check_ecc) + * Phase 6 | SPI 原语(page_read / program_load+exec / block_erase / write_enable / wait_busy) + * Phase 7 | DMA 边界(阈值 32/33/整页) + * Phase 8 | FTL 层(nand_ftl_init / nand_ftl_format) + * Phase 9 | FatFS diskio(可选) + * Phase 10 | 边界条件(offset 越界 / size=0 / 未擦除写入 / 写保护解除) + */ + +#ifndef __GD5F_TEST_H +#define __GD5F_TEST_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include "gd5f2gq5ue.h" + +/* ======================== 阶段使能开关 ======================== */ +/* 取消注释对应阶段即可启用;总开关 ENABLE_GD5F_TESTS 控制是否编译并运行测试。 + * 启用后,defaultTask 中的常规 FTL/FatFS 存储测试会被跳过(独占 NAND)。 + * 注意:测试会擦除/写入若干块,且 Phase 8/9 会格式化 FTL/FatFS,仅用于测试构建。 */ +//#define ENABLE_GD5F_TESTS /* 总开关(由 test_config.h 的 TEST_SUITE_GD5F 自动开启) */ + +//#define ENABLE_GD5F_PHASE1_TESTS +//#define ENABLE_GD5F_PHASE2_TESTS +//#define ENABLE_GD5F_PHASE3_TESTS +//#define ENABLE_GD5F_PHASE4_TESTS +//#define ENABLE_GD5F_PHASE5_TESTS +//#define ENABLE_GD5F_PHASE6_TESTS +//#define ENABLE_GD5F_PHASE7_TESTS +//#define ENABLE_GD5F_PHASE8_TESTS +//#define ENABLE_GD5F_PHASE9_TESTS +//#define ENABLE_GD5F_PHASE10_TESTS + +/* ======================== 测试统计与断言 ======================== */ + +typedef struct { + uint16_t total; + uint16_t passed; + uint16_t failed; +} gd5f_test_stats_t; + +extern gd5f_test_stats_t g_gd5f_test_stats; + +#define GD5F_TEST_CHECK(cond, fmt, ...) do { \ + g_gd5f_test_stats.total++; \ + if (cond) { \ + g_gd5f_test_stats.passed++; \ + DBG_INFO("[PASS] " fmt, ##__VA_ARGS__); \ + } else { \ + g_gd5f_test_stats.failed++; \ + DBG_ERROR("[FAIL] " fmt, ##__VA_ARGS__); \ + } \ +} while (0) + +#define GD5F_TEST_REPORT(name) do { \ + DBG_INFO("=== %s: %d/%d PASSED (failed=%d) ===", \ + name, g_gd5f_test_stats.passed, g_gd5f_test_stats.total, \ + g_gd5f_test_stats.failed); \ +} while (0) + +#if defined(ENABLE_GD5F_TESTS) +void gd5f_test_run(void); +#else +#define gd5f_test_run() ((void)0) +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* __GD5F_TEST_H */ diff --git a/test/gd5f_test_task.c b/test/gd5f_test_task.c new file mode 100644 index 0000000..bd5bb68 --- /dev/null +++ b/test/gd5f_test_task.c @@ -0,0 +1,750 @@ +/****************************************************************************** + * 模块名称:GD5F2GQ5UE Driver Test Task + * 模块功能:GD5F2GQ5UE 驱动全阶段测试入口,统一在 defaultTask 上下文(同步阻塞) + * 中运行。各阶段由 gd5f_test.h 中的 ENABLE_GD5F_PHASE*_TESTS 宏独立开关, + * 覆盖 init/read_id/reset、BBT、页读写、块擦除、ECC、SPI 原语、DMA 边界、 + * FTL 与 FatFS。所有结果通过 GD5F_TEST_CHECK / GD5F_TEST_REPORT 输出串口。 + * 作者:王建锋 + * 创建日期:2026-08-26 + * + * 注意:NAND 不可原地覆盖,且同一页在一次擦除内只能编程一次。因此每个会写入的子 + * 测试开始前都先擦除目标块,避免非法"部分页重编程"导致数据异常或失败。 + ******************************************************************************/ + +#include +#include "test_config.h" +#include "gd5f_test.h" +#include "nand_ftl.h" + +#define DBG_TAG "[NAND-TEST]" +#include "dbg_log.h" +#include "ff.h" + +/* 仅在启用总开关时编译全部测试体(避免未启用时产生未使用函数告警) */ +#ifdef ENABLE_GD5F_TESTS + +gd5f_test_stats_t g_gd5f_test_stats; + +/* 静态缓冲,避免占用任务栈(defaultTask 栈仅 2048 字节)。 + * 不使用大块连续缓冲:多页测试改为逐页读写,降低 RAM 占用。 */ +static uint8_t s_wbuf[GD5F_PAGE_SIZE * 2]; +static uint8_t s_rbuf[GD5F_PAGE_SIZE * 2]; + +/* ------------------------------------------------------------------- + * 辅助:从最高块向下寻找连续 n 个好块,返回其中最高块号;找不到返回 GD5F_TOTAL_BLOCKS + * ------------------------------------------------------------------- */ +static uint32_t gd5f_find_run(int32_t n) +{ + uint32_t usable = gd5f_get_usable_blocks(); + uint32_t top; + int32_t k; + + if (n < 1) { + n = 1; + } + if (usable < (uint32_t)n) { + return usable; /* 管理区不足 */ + } + for (top = usable - 1; top >= (uint32_t)(n - 1) + 1; top--) { + int32_t ok = 1; + for (k = 0; k < n; k++) { + if (gd5f2gq5ue_is_block_bad(top - (uint32_t)k)) { + ok = 0; + break; + } + } + if (ok) { + return top - (uint32_t)(n - 1); /* 返回连续好块的起始块号 */ + } + } + return usable; /* 未找到足够连续的块 */ +} + +/* 辅助:统计当前 BBT 中的坏块数量 */ +static uint32_t gd5f_count_bad(void) +{ + uint32_t b; + uint32_t cnt = 0; + + for (b = 0; b < GD5F_TOTAL_BLOCKS; b++) { + if (gd5f2gq5ue_is_block_bad(b)) { + cnt++; + } + } + return cnt; +} + +/* ------------------------------------------------------------------- + * Phase 1 — 初始化与 ID 识别 + * ------------------------------------------------------------------- */ +#ifdef ENABLE_GD5F_PHASE1_TESTS +static void gd5f_phase1(void) +{ + uint8_t mid = 0, did = 0; + uint8_t s = 0; + int32_t ret; + + DBG_INFO("=== GD5F Phase 1: init / read_id / reset ==="); + + /* TC-GD5F-101: 再次 init(幂等),应成功 */ + ret = gd5f2gq5ue_init(); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-101 init ret=%d", ret); + + /* TC-GD5F-102: read_id 返回 MID=0xC8 DID=0x52 */ + ret = gd5f2gq5ue_read_id(&mid, &did); + GD5F_TEST_CHECK(ret == GD5F_OK && mid == GD5F_MANUFACTURER_ID && + did == GD5F_DEVICE_ID, + "TC-GD5F-102 read_id MID=0x%02X DID=0x%02X", mid, did); + + /* TC-GD5F-103: reset 后 OIP 清零 */ + ret = gd5f2gq5ue_reset(); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-103 reset ret=%d", ret); + gd5f_read_status(&s); + GD5F_TEST_CHECK((s & GD5F_STATUS_OIP) == 0, + "TC-GD5F-103 OIP cleared (status=0x%02X)", s); +} +#endif /* ENABLE_GD5F_PHASE1_TESTS */ + +/* ------------------------------------------------------------------- + * Phase 2 — 坏块管理(BBT) + * ------------------------------------------------------------------- */ +#ifdef ENABLE_GD5F_PHASE2_TESTS +static void gd5f_phase2(void) +{ + uint32_t blk; + uint32_t cnt1, cnt2; + int32_t good, m, c; + /* 快照:init 阶段已扫描构建的出厂 BBT,测试后据此还原(不再重扫) */ + uint32_t orig_bad[GD5F_TOTAL_BLOCKS / 32]; + + DBG_INFO("=== GD5F Phase 2: BBT ==="); + + /* 快照初始化时的 BBT(由 gd5f2gq5ue_init -> gd5f_private_bbt_scan 构建) */ + memset(orig_bad, 0, sizeof(orig_bad)); + for (blk = 0; blk < GD5F_TOTAL_BLOCKS; blk++) { + if (gd5f2gq5ue_is_block_bad(blk) != 0) { + orig_bad[blk >> 5] |= (1U << (blk & 31)); + } + } + + /* TC-GD5F-201: 出厂坏块扫描结果可打印(直接读取 init BBT) */ + gd5f2gq5ue_print_bbt(); + GD5F_TEST_CHECK(1, "TC-GD5F-201 bbt scan/print executed"); + + /* TC-GD5F-202: is_block_bad 好块=0,越界=1 */ + blk = gd5f_find_run(1); + good = gd5f2gq5ue_is_block_bad(blk); + GD5F_TEST_CHECK(good == 0, "TC-GD5F-202 is_block_bad(good=%lu)=%d", blk, good); + GD5F_TEST_CHECK(gd5f2gq5ue_is_block_bad(GD5F_TOTAL_BLOCKS + 1) == 1, + "TC-GD5F-202 is_block_bad(OOB)=1"); + + /* TC-GD5F-203: mark_block_bad / bbt_clear 仅改 RAM BBT(一致性) */ + gd5f2gq5ue_mark_block_bad(blk); + m = gd5f2gq5ue_is_block_bad(blk); + gd5f2gq5ue_bbt_clear(); + c = gd5f2gq5ue_is_block_bad(blk); + GD5F_TEST_CHECK(m == 1 && c == 0, + "TC-GD5F-203 mark=%d clear=%d (RAM only)", m, c); + /* 恢复初始化时的 BBT:按快照还原(不调用已删除的 bbt_rescan) */ + gd5f2gq5ue_bbt_clear(); + for (blk = 0; blk < GD5F_TOTAL_BLOCKS; blk++) { + if ((orig_bad[blk >> 5] & (1U << (blk & 31))) != 0) { + gd5f2gq5ue_mark_block_bad(blk); + } + } + + /* TC-GD5F-204: 初始化 BBT 可直接读取且稳定(无需 rescan) */ + cnt1 = gd5f_count_bad(); + gd5f2gq5ue_print_bbt(); + cnt2 = gd5f_count_bad(); + GD5F_TEST_CHECK(cnt1 == cnt2, + "TC-GD5F-204 init BBT stable %lu == %lu", cnt1, cnt2); + + /* ---------- BBT 持久化测试(保留块池,掉电安全) ---------- */ + { + uint32_t usable = gd5f_get_usable_blocks(); + uint32_t X = gd5f_find_run(1); /* 管理区内的一个好块 */ + uint8_t flash_bbt[GD5F_TOTAL_BLOCKS / 8]; + uint32_t ver = 0; + uint32_t cnt_a, cnt_b; + int32_t marked; + + /* TC-GD5F-205: 保留块池已预留,dhara 可用块数 < 总块数; + * 且测试用块 X 落在管理区内(不会擦到 BBT 存储区) */ + GD5F_TEST_CHECK(usable < (uint32_t)GD5F_TOTAL_BLOCKS, + "TC-GD5F-205 usable=%lu < total=%lu", usable, (uint32_t)GD5F_TOTAL_BLOCKS); + GD5F_TEST_CHECK(X < usable, + "TC-GD5F-205 test block X=%lu < usable=%lu", X, usable); + + /* TC-GD5F-206: 标记坏块后 RAM 生效,且已落盘(闪存副本含该位、版本>=1) */ + gd5f2gq5ue_mark_block_bad(X); + marked = gd5f2gq5ue_is_block_bad(X); + GD5F_TEST_CHECK(marked == 1, "TC-GD5F-206 RAM mark X=%lu -> %d", X, marked); + if (GD5F_OK == gd5f_bbt_dump_flash(flash_bbt, &ver)) { + int32_t flash_bit = (flash_bbt[X >> 3] >> (X & 7)) & 1; + GD5F_TEST_CHECK(flash_bit == 1 && ver >= 1, + "TC-GD5F-206 flash persist X bit=%d ver=%lu", flash_bit, ver); + } else { + GD5F_TEST_CHECK(0, "TC-GD5F-206 dump_flash failed"); + } + + /* TC-GD5F-207: 清空 RAM 后从闪存重载,坏块记录被恢复(验证 load 链路) */ + cnt_a = gd5f_count_bad(); + gd5f2gq5ue_bbt_clear(); + gd5f_bbt_reload(); + marked = gd5f2gq5ue_is_block_bad(X); + cnt_b = gd5f_count_bad(); + GD5F_TEST_CHECK(marked == 1 && cnt_b >= cnt_a, + "TC-GD5F-207 reload X=%lu restored (cnt %lu->%lu)", X, cnt_a, cnt_b); + + /* 清理:还原出厂快照(RAM)并擦除保留池(闪存),设备回到干净态, + * 保证测试可重复且不在 NAND 上留下伪坏块 */ + gd5f2gq5ue_bbt_clear(); + for (blk = 0; blk < GD5F_TOTAL_BLOCKS; blk++) { + if ((orig_bad[blk >> 5] & (1U << (blk & 31))) != 0) { + gd5f2gq5ue_mark_block_bad(blk); + } + } + gd5f_bbt_wipe_pool(); + } +} +#endif /* ENABLE_GD5F_PHASE2_TESTS */ + +/* ------------------------------------------------------------------- + * Phase 3 — 页读写(跨页 / 非对齐 / 多页 / 单字节) + * 每个子测试前先擦除 scratch 块,遵守"一次擦除内每页仅编程一次"。 + * ------------------------------------------------------------------- */ +#ifdef ENABLE_GD5F_PHASE3_TESTS +static void gd5f_phase3(void) +{ + uint32_t blk; + int32_t base; + int32_t ret, ok, i; + + DBG_INFO("=== GD5F Phase 3: page read/write ==="); + blk = gd5f_find_run(1); + base = (int32_t)blk * GD5F_BLOCK_SIZE; + + /* TC-GD5F-301: 整页写读(offset=0, size=2048) */ + gd5f2gq5ue_erase(base, GD5F_BLOCK_SIZE); + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + s_wbuf[i] = (uint8_t)(i & 0xFF); + } + ret = gd5f2gq5ue_write(base, s_wbuf, GD5F_PAGE_SIZE); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-301 write ret=%d", ret); + memset(s_rbuf, 0, GD5F_PAGE_SIZE); + ret = gd5f2gq5ue_read(base, s_rbuf, GD5F_PAGE_SIZE); + ok = 1; + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + if (s_rbuf[i] != (uint8_t)(i & 0xFF)) { ok = 0; break; } + } + GD5F_TEST_CHECK(ret == GD5F_OK && ok, "TC-GD5F-301 readback %s", ok ? "OK" : "MISMATCH"); + + /* TC-GD5F-302: 跨页写读(offset=1000, size=3000) */ + gd5f2gq5ue_erase(base, GD5F_BLOCK_SIZE); + for (i = 0; i < 3000; i++) { + s_wbuf[i] = (uint8_t)((1000 + i) & 0xFF); + } + ret = gd5f2gq5ue_write(base + 1000, s_wbuf, 3000); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-302 write ret=%d", ret); + memset(s_rbuf, 0, 3000); + ret = gd5f2gq5ue_read(base + 1000, s_rbuf, 3000); + ok = 1; + for (i = 0; i < 3000; i++) { + if (s_rbuf[i] != (uint8_t)((1000 + i) & 0xFF)) { ok = 0; break; } + } + GD5F_TEST_CHECK(ret == GD5F_OK && ok, "TC-GD5F-302 cross-page %s", ok ? "OK" : "MISMATCH"); + + /* TC-GD5F-303: 非对齐 offset=1, size=2047 */ + gd5f2gq5ue_erase(base, GD5F_BLOCK_SIZE); + for (i = 0; i < 2047; i++) { + s_wbuf[i] = (uint8_t)((1 + i) & 0xFF); + } + ret = gd5f2gq5ue_write(base + 1, s_wbuf, 2047); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-303 write ret=%d", ret); + memset(s_rbuf, 0, 2047); + ret = gd5f2gq5ue_read(base + 1, s_rbuf, 2047); + ok = 1; + for (i = 0; i < 2047; i++) { + if (s_rbuf[i] != (uint8_t)((1 + i) & 0xFF)) { ok = 0; break; } + } + GD5F_TEST_CHECK(ret == GD5F_OK && ok, "TC-GD5F-303 unaligned %s", ok ? "OK" : "MISMATCH"); + + /* TC-GD5F-304: 多页顺序写读(10 页,逐页校验,避免大缓冲占用 RAM) */ + { + int32_t p; + int32_t off; + gd5f2gq5ue_erase(base, GD5F_BLOCK_SIZE); + for (p = 0; p < 10; p++) { + off = p * GD5F_PAGE_SIZE; + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + s_wbuf[i] = (uint8_t)((off + i) & 0xFF); + } + ret = gd5f2gq5ue_write(base + off, s_wbuf, GD5F_PAGE_SIZE); + if (ret != GD5F_OK) break; + } + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-304 write ret=%d", ret); + ok = 1; + for (p = 0; p < 10 && ok; p++) { + off = p * GD5F_PAGE_SIZE; + memset(s_rbuf, 0, GD5F_PAGE_SIZE); + gd5f2gq5ue_read(base + off, s_rbuf, GD5F_PAGE_SIZE); + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + if (s_rbuf[i] != (uint8_t)((off + i) & 0xFF)) { ok = 0; break; } + } + } + GD5F_TEST_CHECK(ok, "TC-GD5F-304 multi-page %s", ok ? "OK" : "MISMATCH"); + } + + /* TC-GD5F-305: 随机单字节访问(独立擦除后写单字节,余字节应保持 0xFF) */ + { + uint32_t seeds[7]; + int32_t k, allok = 1; + uint8_t val, r = 0; + + seeds[0] = 0; + seeds[1] = 1; + seeds[2] = GD5F_PAGE_SIZE - 1; + seeds[3] = GD5F_PAGE_SIZE; + seeds[4] = GD5F_PAGE_SIZE + 100; + seeds[5] = 3000; + seeds[6] = 20479; + + gd5f2gq5ue_erase(base, GD5F_BLOCK_SIZE); + for (k = 0; k < 7; k++) { + val = (uint8_t)((seeds[k] * 7 + 3) & 0xFF); + ret = gd5f2gq5ue_write(base + (int32_t)seeds[k], &val, 1); + if (ret != GD5F_OK) { allok = 0; break; } + r = 0; + ret = gd5f2gq5ue_read(base + (int32_t)seeds[k], &r, 1); + if (ret != GD5F_OK || r != val) { allok = 0; break; } + } + GD5F_TEST_CHECK(allok, "TC-GD5F-305 random single-byte"); + } +} +#endif /* ENABLE_GD5F_PHASE3_TESTS */ + +/* ------------------------------------------------------------------- + * Phase 4 — 块擦除 + * ------------------------------------------------------------------- */ +#ifdef ENABLE_GD5F_PHASE4_TESTS +static void gd5f_phase4(void) +{ + uint32_t blk, blk3; + int32_t base, base3; + int32_t ret, ok, i; + int32_t off; + + DBG_INFO("=== GD5F Phase 4: block erase ==="); + blk = gd5f_find_run(1); + base = (int32_t)blk * GD5F_BLOCK_SIZE; + + /* TC-GD5F-401: 单块擦除后读全 0xFF */ + ret = gd5f2gq5ue_erase(base, GD5F_BLOCK_SIZE); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-401 erase ret=%d", ret); + ok = 1; + for (off = 0; off < (int32_t)GD5F_BLOCK_SIZE; off += GD5F_PAGE_SIZE) { + memset(s_rbuf, 0, GD5F_PAGE_SIZE); + gd5f2gq5ue_read(base + off, s_rbuf, GD5F_PAGE_SIZE); + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + if (s_rbuf[i] != 0xFF) { ok = 0; break; } + } + if (!ok) break; + } + GD5F_TEST_CHECK(ok, "TC-GD5F-401 erased block all 0xFF"); + + /* TC-GD5F-402: 多块擦除(3 块连续) */ + blk3 = gd5f_find_run(3); + if (blk3 == GD5F_TOTAL_BLOCKS) { + GD5F_TEST_CHECK(0, "TC-GD5F-402 no 3 consecutive good blocks"); + } else { + base3 = (int32_t)blk3 * GD5F_BLOCK_SIZE; + ret = gd5f2gq5ue_erase(base3, 3 * GD5F_BLOCK_SIZE); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-402 multi-erase(3) ret=%d", ret); + ok = 1; + for (off = 0; off < (int32_t)(3 * GD5F_BLOCK_SIZE); off += GD5F_PAGE_SIZE) { + memset(s_rbuf, 0, GD5F_PAGE_SIZE); + gd5f2gq5ue_read(base3 + off, s_rbuf, GD5F_PAGE_SIZE); + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + if (s_rbuf[i] != 0xFF) { ok = 0; break; } + } + if (!ok) break; + } + GD5F_TEST_CHECK(ok, "TC-GD5F-402 multi-erase all 0xFF"); + } + + /* TC-GD5F-403: 非块对齐应返回错误 */ + ret = gd5f2gq5ue_erase(base + 100, GD5F_BLOCK_SIZE); + GD5F_TEST_CHECK(ret != GD5F_OK, + "TC-GD5F-403 offset unaligned rejected (ret=%d)", ret); + ret = gd5f2gq5ue_erase(base, 100); + GD5F_TEST_CHECK(ret != GD5F_OK, + "TC-GD5F-403 size unaligned rejected (ret=%d)", ret); + + /* TC-GD5F-404: 擦除-写-读 完整闭环 */ + gd5f2gq5ue_erase(base, GD5F_BLOCK_SIZE); + memset(s_wbuf, 0xA5, GD5F_PAGE_SIZE); + ret = gd5f2gq5ue_write(base, s_wbuf, GD5F_PAGE_SIZE); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-404 write ret=%d", ret); + memset(s_rbuf, 0, GD5F_PAGE_SIZE); + gd5f2gq5ue_read(base, s_rbuf, GD5F_PAGE_SIZE); + ok = 1; + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + if (s_rbuf[i] != 0xA5) { ok = 0; break; } + } + GD5F_TEST_CHECK(ok, "TC-GD5F-404 erase-write-read loop %s", ok ? "OK" : "MISMATCH"); +} +#endif /* ENABLE_GD5F_PHASE4_TESTS */ + +/* ------------------------------------------------------------------- + * Phase 5 — ECC 验证 + * ------------------------------------------------------------------- */ +#ifdef ENABLE_GD5F_PHASE5_TESTS +static void gd5f_phase5(void) +{ + uint32_t blk; + uint32_t page; + int32_t base; + int32_t ret, ecc, ok, i; + + DBG_INFO("=== GD5F Phase 5: ECC ==="); + blk = gd5f_find_run(1); + base = (int32_t)blk * GD5F_BLOCK_SIZE; + page = blk * GD5F_PAGES_PER_BLOCK; + + /* TC-GD5F-501: ECC 开启下写读正确,check_ecc 无错 */ + gd5f2gq5ue_erase(base, GD5F_BLOCK_SIZE); + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + s_wbuf[i] = (uint8_t)(i & 0x55); + } + ret = gd5f2gq5ue_write(base, s_wbuf, GD5F_PAGE_SIZE); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-501 write ret=%d", ret); + ret = gd5f_page_read(page); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-501 page_read ret=%d", ret); + ecc = gd5f_check_ecc(); + GD5F_TEST_CHECK(ecc == GD5F_OK, "TC-GD5F-501 check_ecc=%d", ecc); + memset(s_rbuf, 0, GD5F_PAGE_SIZE); + gd5f_read_from_cache(0, s_rbuf, GD5F_PAGE_SIZE); + ok = 1; + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + if (s_rbuf[i] != (uint8_t)(i & 0x55)) { ok = 0; break; } + } + GD5F_TEST_CHECK(ok, "TC-GD5F-501 data correct %s", ok ? "OK" : "MISMATCH"); + + /* TC-GD5F-502: 干净页 check_ecc 返回 OK */ + ret = gd5f_page_read(page); + ecc = gd5f_check_ecc(); + GD5F_TEST_CHECK(ret == GD5F_OK && ecc == GD5F_OK, + "TC-GD5F-502 clean read ecc=%d", ecc); +} +#endif /* ENABLE_GD5F_PHASE5_TESTS */ + +/* ------------------------------------------------------------------- + * Phase 6 — SPI 原语(FTL 共享) + * ------------------------------------------------------------------- */ +#ifdef ENABLE_GD5F_PHASE6_TESTS +static void gd5f_phase6(void) +{ + uint32_t blk; + uint32_t page0; + int32_t base; + int32_t ret, ok, i; + uint8_t s = 0; + + DBG_INFO("=== GD5F Phase 6: SPI primitives ==="); + blk = gd5f_find_run(1); + base = (int32_t)blk * GD5F_BLOCK_SIZE; + page0 = blk * GD5F_PAGES_PER_BLOCK; + + /* TC-GD5F-603: gd5f_block_erase 原语(传块号) */ + ret = gd5f_block_erase(blk); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-603 block_erase ret=%d", ret); + ok = 1; + for (i = 0; i < GD5F_PAGES_PER_BLOCK; i++) { + gd5f_page_read(page0 + (uint32_t)i); + gd5f_read_from_cache(0, s_rbuf, GD5F_PAGE_SIZE); + for (int32_t j = 0; j < GD5F_PAGE_SIZE; j++) { + if (s_rbuf[j] != 0xFF) { ok = 0; break; } + } + if (!ok) break; + } + GD5F_TEST_CHECK(ok, "TC-GD5F-603 erased block all 0xFF"); + + /* TC-GD5F-602: program_load + program_exec 组合(先写使能) */ + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + s_wbuf[i] = (uint8_t)(i & 0xAA); + } + gd5f_write_enable(); + ret = gd5f_program_load(0, s_wbuf, GD5F_PAGE_SIZE); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-602 program_load ret=%d", ret); + ret = gd5f_program_exec(page0); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-602 program_exec ret=%d", ret); + /* 用高层读验证 */ + memset(s_rbuf, 0, GD5F_PAGE_SIZE); + ret = gd5f2gq5ue_read(base, s_rbuf, GD5F_PAGE_SIZE); + ok = 1; + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + if (s_rbuf[i] != (uint8_t)(i & 0xAA)) { ok = 0; break; } + } + GD5F_TEST_CHECK(ret == GD5F_OK && ok, + "TC-GD5F-602 verify (high-level read) %s", ok ? "OK" : "MISMATCH"); + + /* TC-GD5F-601: page_read + read_from_cache 组合 */ + ret = gd5f_page_read(page0); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-601 page_read ret=%d", ret); + memset(s_rbuf, 0, GD5F_PAGE_SIZE); + ret = gd5f_read_from_cache(0, s_rbuf, GD5F_PAGE_SIZE); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-601 read_from_cache ret=%d", ret); + ok = 1; + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + if (s_rbuf[i] != (uint8_t)(i & 0xAA)) { ok = 0; break; } + } + GD5F_TEST_CHECK(ok, "TC-GD5F-601 read via primitives %s", ok ? "OK" : "MISMATCH"); + + /* TC-GD5F-604: write_enable 后置 WEL 位 */ + gd5f_write_enable(); + gd5f_read_status(&s); + GD5F_TEST_CHECK((s & GD5F_STATUS_WEL) != 0, + "TC-GD5F-604 WEL set (status=0x%02X)", s); + + /* TC-GD5F-605: wait_busy 正常返回 */ + ret = gd5f_wait_busy(1000); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-605 wait_busy ret=%d", ret); +} +#endif /* ENABLE_GD5F_PHASE6_TESTS */ + +/* ------------------------------------------------------------------- + * Phase 7 — DMA 边界(阈值 32 字节) + * ------------------------------------------------------------------- */ +#ifdef ENABLE_GD5F_PHASE7_TESTS +static void gd5f_phase7(void) +{ + uint32_t blk; + int32_t base; + int32_t ret, ok, i; + + DBG_INFO("=== GD5F Phase 7: DMA boundary (threshold 32) ==="); + blk = gd5f_find_run(1); + base = (int32_t)blk * GD5F_BLOCK_SIZE; + + /* TC-GD5F-701: size=32 轮询路径 */ + gd5f2gq5ue_erase(base, GD5F_BLOCK_SIZE); + for (i = 0; i < 32; i++) { + s_wbuf[i] = (uint8_t)(i + 0x10); + } + ret = gd5f2gq5ue_write(base, s_wbuf, 32); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-701 write(32) ret=%d", ret); + memset(s_rbuf, 0, 32); + ret = gd5f2gq5ue_read(base, s_rbuf, 32); + ok = 1; + for (i = 0; i < 32; i++) { + if (s_rbuf[i] != (uint8_t)(i + 0x10)) { ok = 0; break; } + } + GD5F_TEST_CHECK(ok, "TC-GD5F-701 polling path %s", ok ? "OK" : "MISMATCH"); + + /* TC-GD5F-702: size=33 DMA 路径 */ + gd5f2gq5ue_erase(base, GD5F_BLOCK_SIZE); + for (i = 0; i < 33; i++) { + s_wbuf[i] = (uint8_t)(i + 0x20); + } + ret = gd5f2gq5ue_write(base, s_wbuf, 33); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-702 write(33) ret=%d", ret); + memset(s_rbuf, 0, 33); + ret = gd5f2gq5ue_read(base, s_rbuf, 33); + ok = 1; + for (i = 0; i < 33; i++) { + if (s_rbuf[i] != (uint8_t)(i + 0x20)) { ok = 0; break; } + } + GD5F_TEST_CHECK(ok, "TC-GD5F-702 DMA path %s", ok ? "OK" : "MISMATCH"); + + /* TC-GD5F-703: size=2048 整页 DMA 路径 */ + gd5f2gq5ue_erase(base, GD5F_BLOCK_SIZE); + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + s_wbuf[i] = (uint8_t)(i & 0xFF); + } + ret = gd5f2gq5ue_write(base, s_wbuf, GD5F_PAGE_SIZE); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-703 write(2048) ret=%d", ret); + memset(s_rbuf, 0, GD5F_PAGE_SIZE); + ret = gd5f2gq5ue_read(base, s_rbuf, GD5F_PAGE_SIZE); + ok = 1; + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + if (s_rbuf[i] != (uint8_t)(i & 0xFF)) { ok = 0; break; } + } + GD5F_TEST_CHECK(ok, "TC-GD5F-703 DMA whole page %s", ok ? "OK" : "MISMATCH"); +} +#endif /* ENABLE_GD5F_PHASE7_TESTS */ + +/* ------------------------------------------------------------------- + * Phase 8 — FTL 层 + * ------------------------------------------------------------------- */ +#ifdef ENABLE_GD5F_PHASE8_TESTS +static void gd5f_phase8(void) +{ + int32_t ret; + + DBG_INFO("=== GD5F Phase 8: FTL ==="); + + /* TC-GD5F-801: nand_ftl_init */ + ret = nand_ftl_init(); + GD5F_TEST_CHECK(ret == 0, "TC-GD5F-801 nand_ftl_init ret=%d", ret); + + /* TC-GD5F-802: nand_ftl_format */ + ret = nand_ftl_format(); + GD5F_TEST_CHECK(ret == 0, "TC-GD5F-802 nand_ftl_format ret=%d", ret); +} +#endif /* ENABLE_GD5F_PHASE8_TESTS */ + +/* ------------------------------------------------------------------- + * Phase 9 — FatFS diskio(可选) + * ------------------------------------------------------------------- */ +#ifdef ENABLE_GD5F_PHASE9_TESTS +static void gd5f_phase9(void) +{ + FATFS fs; + FIL fil; + UINT bw = 0, br = 0; + FRESULT res; + const char *ts = "GD5F-FatFS-OK"; + char rb[32]; + static uint8_t work[FF_MAX_SS]; + + DBG_INFO("=== GD5F Phase 9: FatFS diskio ==="); + + nand_ftl_init(); + + res = f_mount(&fs, "", 1); + if (res == FR_NO_FILESYSTEM) { + MKFS_PARM opts = { FM_FAT32, 0, 0, 0, 0 }; + res = f_mkfs("", &opts, work, sizeof(work)); + res = f_mount(&fs, "", 1); + } + GD5F_TEST_CHECK(res == FR_OK, "TC-GD5F-901 f_mount res=%d", res); + + if (res == FR_OK) { + res = f_open(&fil, "gd5f_t.txt", FA_CREATE_ALWAYS | FA_WRITE); + GD5F_TEST_CHECK(res == FR_OK, "TC-GD5F-901 f_open(w) res=%d", res); + if (res == FR_OK) { + f_write(&fil, ts, (UINT)strlen(ts), &bw); + GD5F_TEST_CHECK(bw == (UINT)strlen(ts), "TC-GD5F-901 f_write %u", bw); + f_close(&fil); + + res = f_open(&fil, "gd5f_t.txt", FA_READ); + GD5F_TEST_CHECK(res == FR_OK, "TC-GD5F-901 f_open(r) res=%d", res); + if (res == FR_OK) { + memset(rb, 0, sizeof(rb)); + f_read(&fil, rb, (UINT)strlen(ts), &br); + f_close(&fil); + GD5F_TEST_CHECK(br == (UINT)strlen(ts) && + memcmp(ts, rb, br) == 0, + "TC-GD5F-901 verify \"%.*s\"", (int)br, rb); + } + } + f_unlink("gd5f_t.txt"); + } +} +#endif /* ENABLE_GD5F_PHASE9_TESTS */ + +/* ------------------------------------------------------------------- + * Phase 10 — 边界条件 + * ------------------------------------------------------------------- */ +#ifdef ENABLE_GD5F_PHASE10_TESTS +static void gd5f_phase10(void) +{ + uint32_t blk; + int32_t base; + int32_t ret, i, ne, ok; + + DBG_INFO("=== GD5F Phase 10: boundary conditions ==="); + blk = gd5f_find_run(1); + base = (int32_t)blk * GD5F_BLOCK_SIZE; + + /* TC-GD5F-1001: offset 越界应返回错误 */ + ret = gd5f2gq5ue_read((int32_t)GD5F_TOTAL_SIZE, s_rbuf, 1); + GD5F_TEST_CHECK(ret != GD5F_OK, + "TC-GD5F-1001 read OOB rejected (ret=%d)", ret); + ret = gd5f2gq5ue_write((int32_t)GD5F_TOTAL_SIZE, s_wbuf, 1); + GD5F_TEST_CHECK(ret != GD5F_OK, + "TC-GD5F-1001 write OOB rejected (ret=%d)", ret); + + /* TC-GD5F-1002: size=0 不崩溃,返回 GD5F_OK(空操作) */ + ret = gd5f2gq5ue_read(base, s_rbuf, 0); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-1002 read size=0 ret=%d", ret); + ret = gd5f2gq5ue_write(base, s_wbuf, 0); + GD5F_TEST_CHECK(ret == GD5F_OK, "TC-GD5F-1002 write size=0 ret=%d", ret); + + /* TC-GD5F-1003: 写前未擦除 → 数据不可预期(NAND 不可原地覆盖) + * 先擦除写入全 0x00,再不清擦写入全 0xFF(需要 0→1,物理不可行), + * 读回应仍 != 0xFF。 */ + gd5f2gq5ue_erase(base, GD5F_BLOCK_SIZE); + memset(s_wbuf, 0x00, GD5F_PAGE_SIZE); + gd5f2gq5ue_write(base, s_wbuf, GD5F_PAGE_SIZE); + memset(s_wbuf, 0xFF, GD5F_PAGE_SIZE); + gd5f2gq5ue_write(base, s_wbuf, GD5F_PAGE_SIZE); /* 未擦除,0→1 不可能 */ + memset(s_rbuf, 0, GD5F_PAGE_SIZE); + gd5f2gq5ue_read(base, s_rbuf, GD5F_PAGE_SIZE); + ne = 0; + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + if (s_rbuf[i] != 0xFF) ne++; + } + GD5F_TEST_CHECK(ne > 0, + "TC-GD5F-1003 write-without-erase not applied (bytes!=0xFF: %d)", ne); + + /* TC-GD5F-1004: 写保护解除验证(init 后 Protect=0x00,可正常编程) */ + gd5f2gq5ue_erase(base, GD5F_BLOCK_SIZE); + memset(s_wbuf, 0x5A, GD5F_PAGE_SIZE); + ret = gd5f2gq5ue_write(base, s_wbuf, GD5F_PAGE_SIZE); + memset(s_rbuf, 0, GD5F_PAGE_SIZE); + gd5f2gq5ue_read(base, s_rbuf, GD5F_PAGE_SIZE); + ok = 1; + for (i = 0; i < GD5F_PAGE_SIZE; i++) { + if (s_rbuf[i] != 0x5A) { ok = 0; break; } + } + GD5F_TEST_CHECK(ret == GD5F_OK && ok, + "TC-GD5F-1004 write unprotected (ret=%d) %s", ret, ok ? "OK" : "FAIL"); +} +#endif /* ENABLE_GD5F_PHASE10_TESTS */ + +/* ------------------------------------------------------------------- + * 入口:按启用的阶段顺序执行并汇总 + * ------------------------------------------------------------------- */ +void gd5f_test_run(void) +{ + memset(&g_gd5f_test_stats, 0, sizeof(g_gd5f_test_stats)); + DBG_INFO("########## GD5F2GQ5UE Test Start ##########"); + +#ifdef ENABLE_GD5F_PHASE1_TESTS + gd5f_phase1(); +#endif +#ifdef ENABLE_GD5F_PHASE2_TESTS + gd5f_phase2(); +#endif +#ifdef ENABLE_GD5F_PHASE3_TESTS + gd5f_phase3(); +#endif +#ifdef ENABLE_GD5F_PHASE4_TESTS + gd5f_phase4(); +#endif +#ifdef ENABLE_GD5F_PHASE5_TESTS + gd5f_phase5(); +#endif +#ifdef ENABLE_GD5F_PHASE6_TESTS + gd5f_phase6(); +#endif +#ifdef ENABLE_GD5F_PHASE7_TESTS + gd5f_phase7(); +#endif +#ifdef ENABLE_GD5F_PHASE8_TESTS + gd5f_phase8(); +#endif +#ifdef ENABLE_GD5F_PHASE9_TESTS + gd5f_phase9(); +#endif +#ifdef ENABLE_GD5F_PHASE10_TESTS + gd5f_phase10(); +#endif + + GD5F_TEST_REPORT("GD5F2GQ5UE Tests"); + DBG_INFO("########## GD5F2GQ5UE Test Done ##########"); +} + +#endif /* ENABLE_GD5F_TESTS */ diff --git a/test/net_test_task.c b/test/net_test_task.c index 43dc844..b2b30b5 100644 --- a/test/net_test_task.c +++ b/test/net_test_task.c @@ -51,15 +51,15 @@ * =================================================================== */ #ifdef ENABLE_PHASE6_TESTS -static void phase6_run(int sockfd) { +static void phase6_run(int32_t sockfd) { /* * 单连接 TCP Echo:NET_MSG_DONTWAIT 非阻塞收,原样回显。 * 连接断开由 net 层自动重监听(auto_relisten),应用层不维护生命周期; * 字节完整性由 PC 侧校验(data == test_data),固件不做每连接自检。 */ - int n = net_recv(sockfd, s_rx_buf, TEST_BUF_SIZE, NET_MSG_DONTWAIT); + int32_t n = net_recv(sockfd, s_rx_buf, TEST_BUF_SIZE, NET_MSG_DONTWAIT); if (n > 0) { - int sent = net_send(sockfd, s_rx_buf, n, 0); + int32_t sent = net_send(sockfd, s_rx_buf, n, 0); if (sent != n) { DBG_ERROR("echo: send %d != recv %d", sent, n); } @@ -119,9 +119,9 @@ static uint32_t phase7_cksum(const uint8_t *p, uint32_t n) { /* 单次 TCP Client 会话:对应 TC 701 主体 / TC 702 单轮。 * 每轮使用不同本地端口 local_port 规避 TIME_WAIT。 * 返回 1 表示全流程通过,0 表示中途失败(已记录 TEST_CHECK)。 */ -static int phase7_session(uint16_t local_port) +static int32_t phase7_session(uint16_t local_port) { - int sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM, 0); + int32_t sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM, 0); if (sockfd < 0) { TEST_CHECK(0, "TC701 socket create"); return 0; @@ -149,14 +149,14 @@ static int phase7_session(uint16_t local_port) DBG_INFO("TC701 connecting %s:%d (local port %d)", PHASE7_REMOTE_IP, PHASE7_REMOTE_PORT, local_port); - int rc = net_connect(sockfd, (struct net_sockaddr *)&raddr, sizeof(raddr)); + int32_t rc = net_connect(sockfd, (struct net_sockaddr *)&raddr, sizeof(raddr)); TEST_CHECK(rc == 0, "TC701 connect issued (ret=%d)", rc); if (rc != 0) { net_close(sockfd); return 0; } /* 等待连接建立:轮询 socket 状态(ESTABLISHED)。 * 用 net_select 触发 net_poll 并作为 100ms 节拍,状态以 net_get_sock 为准。 */ { - int connected = 0; + int32_t connected = 0; uint32_t tick = HAL_GetTick(); while (HAL_GetTick() - tick < PHASE7_CONN_TIMEOUT_MS) { net_sock_t *sk = net_get_sock(sockfd); @@ -175,16 +175,16 @@ static int phase7_session(uint16_t local_port) /* 数据交换:64B 递增模式 */ uint8_t tx[PHASE7_EXCH_SIZE]; uint8_t rx[PHASE7_EXCH_SIZE]; - for (int i = 0; i < PHASE7_EXCH_SIZE; i++) tx[i] = (uint8_t)i; + for (int32_t i = 0; i < PHASE7_EXCH_SIZE; i++) tx[i] = (uint8_t)i; - int sent = net_send(sockfd, tx, PHASE7_EXCH_SIZE, 0); + int32_t sent = net_send(sockfd, tx, PHASE7_EXCH_SIZE, 0); TEST_CHECK(sent == PHASE7_EXCH_SIZE, "TC701 send %dB (sent=%d)", PHASE7_EXCH_SIZE, sent); if (sent != PHASE7_EXCH_SIZE) { net_close(sockfd); return 0; } - int recvd = 0; + int32_t recvd = 0; uint32_t tick = HAL_GetTick(); while (recvd < PHASE7_EXCH_SIZE) { - int n = net_recv(sockfd, rx + recvd, PHASE7_EXCH_SIZE - recvd, NET_MSG_DONTWAIT); + int32_t n = net_recv(sockfd, rx + recvd, PHASE7_EXCH_SIZE - recvd, NET_MSG_DONTWAIT); if (n > 0) { recvd += n; } else if (n == 0) { @@ -199,10 +199,10 @@ static int phase7_session(uint16_t local_port) if (HAL_GetTick() - tick > PHASE7_CONN_TIMEOUT_MS) break; } - int ok = (recvd == PHASE7_EXCH_SIZE); + int32_t ok = (recvd == PHASE7_EXCH_SIZE); if (ok) { - int bad = 0; - for (int i = 0; i < PHASE7_EXCH_SIZE; i++) { + int32_t bad = 0; + for (int32_t i = 0; i < PHASE7_EXCH_SIZE; i++) { if (rx[i] != (uint8_t)i) { bad = 1; break; } } TEST_CHECK(!bad, "TC701 byte-compare %dB", PHASE7_EXCH_SIZE); @@ -220,7 +220,7 @@ static int phase7_session(uint16_t local_port) * 701/702 要求服务端在线,703 要求服务端离线,故 703 应在独立的一轮中运行)。 */ static void phase7_test_timeout(void) { - int sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM, 0); + int32_t sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM, 0); if (sockfd < 0) { TEST_CHECK(0, "TC703 socket create"); return; } struct net_sockaddr_in raddr; @@ -231,11 +231,11 @@ static void phase7_test_timeout(void) DBG_INFO("TC703 connecting %s:%d (expect TIMEOUT, PC server must be OFF)", PHASE7_REMOTE_IP, PHASE7_REMOTE_PORT); - int rc = net_connect(sockfd, (struct net_sockaddr *)&raddr, sizeof(raddr)); + int32_t rc = net_connect(sockfd, (struct net_sockaddr *)&raddr, sizeof(raddr)); TEST_CHECK(rc == 0, "TC703 connect issued (ret=%d)", rc); if (rc != 0) { net_close(sockfd); return; } - int established = 0; + int32_t established = 0; uint32_t tick = HAL_GetTick(); while (HAL_GetTick() - tick < PHASE7_TIMEOUT_TEST_MS) { net_sock_t *sk = net_get_sock(sockfd); @@ -262,19 +262,19 @@ static void phase7_test_timeout(void) #define PHASE7_MULTI_BASE_PORT 51000 #endif -static int phase7_session_multi(void) +static int32_t phase7_session_multi(void) { - int sockfd[PHASE7_MULTI_COUNT]; + int32_t sockfd[PHASE7_MULTI_COUNT]; uint8_t tx[PHASE7_EXCH_SIZE]; uint8_t rx[PHASE7_EXCH_SIZE]; - for (int i = 0; i < PHASE7_EXCH_SIZE; i++) tx[i] = (uint8_t)i; + for (int32_t i = 0; i < PHASE7_EXCH_SIZE; i++) tx[i] = (uint8_t)i; /* 1) 打开并连接所有 Socket(各自绑定不同本地端口规避 TIME_WAIT) */ - for (int i = 0; i < PHASE7_MULTI_COUNT; i++) { + for (int32_t i = 0; i < PHASE7_MULTI_COUNT; i++) { sockfd[i] = net_socket(NET_AF_INET, NET_SOCK_STREAM, 0); if (sockfd[i] < 0) { TEST_CHECK(0, "TC704 socket create #%d", i); - for (int j = 0; j < i; j++) net_close(sockfd[j]); + for (int32_t j = 0; j < i; j++) net_close(sockfd[j]); return 0; } struct net_sockaddr_in laddr; @@ -284,7 +284,7 @@ static int phase7_session_multi(void) if (net_bind(sockfd[i], (struct net_sockaddr *)&laddr, sizeof(laddr)) < 0) { TEST_CHECK(0, "TC704 bind #%d port %d", i, PHASE7_MULTI_BASE_PORT + i); net_close(sockfd[i]); - for (int j = 0; j < i; j++) net_close(sockfd[j]); + for (int32_t j = 0; j < i; j++) net_close(sockfd[j]); return 0; } struct net_sockaddr_in raddr; @@ -292,11 +292,11 @@ static int phase7_session_multi(void) raddr.sin_family = NET_AF_INET; raddr.sin_port = net_htons((uint16_t)PHASE7_REMOTE_PORT); raddr.sin_addr.s_addr = net_inet_addr(PHASE7_REMOTE_IP); - int rc = net_connect(sockfd[i], (struct net_sockaddr *)&raddr, sizeof(raddr)); + int32_t rc = net_connect(sockfd[i], (struct net_sockaddr *)&raddr, sizeof(raddr)); TEST_CHECK(rc == 0, "TC704 connect #%d issued (ret=%d)", i, rc); if (rc != 0) { net_close(sockfd[i]); - for (int j = 0; j < i; j++) net_close(sockfd[j]); + for (int32_t j = 0; j < i; j++) net_close(sockfd[j]); return 0; } } @@ -304,10 +304,10 @@ static int phase7_session_multi(void) /* 2) 等待所有连接建立(或超时) */ { uint32_t tick = HAL_GetTick(); - int all_up = 0; + int32_t all_up = 0; while (HAL_GetTick() - tick < PHASE7_CONN_TIMEOUT_MS) { all_up = 1; - for (int i = 0; i < PHASE7_MULTI_COUNT; i++) { + for (int32_t i = 0; i < PHASE7_MULTI_COUNT; i++) { net_sock_t *sk = net_get_sock(sockfd[i]); if (!sk || sk->state != NET_SOCK_STATE_ESTABLISHED) { all_up = 0; break; } } @@ -316,7 +316,7 @@ static int phase7_session_multi(void) } TEST_CHECK(all_up, "TC704 all %d sockets established within %dms", PHASE7_MULTI_COUNT, PHASE7_CONN_TIMEOUT_MS); if (!all_up) { - for (int i = 0; i < PHASE7_MULTI_COUNT; i++) { + for (int32_t i = 0; i < PHASE7_MULTI_COUNT; i++) { net_sock_t *sk = net_get_sock(sockfd[i]); if (sk) DBG_ERROR("TC704 socket #%d state=%d", i, sk->state); net_close(sockfd[i]); @@ -326,18 +326,18 @@ static int phase7_session_multi(void) } /* 3) 每路独立收发回显 */ - int ok = 1; - for (int i = 0; i < PHASE7_MULTI_COUNT; i++) { - int sent = net_send(sockfd[i], tx, PHASE7_EXCH_SIZE, 0); + int32_t ok = 1; + for (int32_t i = 0; i < PHASE7_MULTI_COUNT; i++) { + int32_t sent = net_send(sockfd[i], tx, PHASE7_EXCH_SIZE, 0); if (sent != PHASE7_EXCH_SIZE) { TEST_CHECK(0, "TC704 socket #%d send %dB (sent=%d)", i, PHASE7_EXCH_SIZE, sent); ok = 0; continue; } - int recvd = 0; + int32_t recvd = 0; uint32_t tick = HAL_GetTick(); while (recvd < PHASE7_EXCH_SIZE) { - int n = net_recv(sockfd[i], rx + recvd, PHASE7_EXCH_SIZE - recvd, NET_MSG_DONTWAIT); + int32_t n = net_recv(sockfd[i], rx + recvd, PHASE7_EXCH_SIZE - recvd, NET_MSG_DONTWAIT); if (n > 0) recvd += n; else if (n == 0) break; else if (n < 0 && net_get_errno() == NET_ERR_WOULDBLOCK) { @@ -347,10 +347,10 @@ static int phase7_session_multi(void) } else break; if (HAL_GetTick() - tick > PHASE7_CONN_TIMEOUT_MS) break; } - int sock_ok = (recvd == PHASE7_EXCH_SIZE); + int32_t sock_ok = (recvd == PHASE7_EXCH_SIZE); if (sock_ok) { - int bad = 0; - for (int k = 0; k < PHASE7_EXCH_SIZE; k++) if (rx[k] != (uint8_t)k) { bad = 1; break; } + int32_t bad = 0; + for (int32_t k = 0; k < PHASE7_EXCH_SIZE; k++) if (rx[k] != (uint8_t)k) { bad = 1; break; } TEST_CHECK(!bad, "TC704 socket #%d echo %dB OK", i, PHASE7_EXCH_SIZE); } else { TEST_CHECK(0, "TC704 socket #%d recv %dB (expect %d)", i, recvd, PHASE7_EXCH_SIZE); @@ -359,7 +359,7 @@ static int phase7_session_multi(void) } /* 4) 关闭所有 */ - for (int i = 0; i < PHASE7_MULTI_COUNT; i++) net_close(sockfd[i]); + for (int32_t i = 0; i < PHASE7_MULTI_COUNT; i++) net_close(sockfd[i]); return ok; } @@ -373,8 +373,8 @@ void net_layer_client_test_main(void) DBG_INFO("--- TC 702: close/reconnect x%d ---", PHASE7_LOOP_COUNT); { - int all_ok = 1; - for (int i = 0; i < PHASE7_LOOP_COUNT; i++) { + int32_t all_ok = 1; + for (int32_t i = 0; i < PHASE7_LOOP_COUNT; i++) { if (!phase7_session((uint16_t)(PHASE7_LOCAL_PORT + 1 + i))) all_ok = 0; } TEST_CHECK(all_ok, "TC702 reconnect x%d all passed", PHASE7_LOOP_COUNT); @@ -445,7 +445,7 @@ static uint8_t s_tx_buf[TEST_BUF_SIZE]; static uint32_t phase8_crc32_step(uint32_t crc, const uint8_t *data, uint32_t len) { for (uint32_t i = 0; i < len; i++) { crc ^= data[i]; - for (int k = 0; k < 8; k++) { + for (int32_t k = 0; k < 8; k++) { crc = (crc & 1u) ? (crc >> 1) ^ 0xEDB88320u : (crc >> 1); } } @@ -464,7 +464,7 @@ static void phase8_fill(uint8_t *buf, uint32_t size, uint32_t offset) { /* 仅 ECHO/RECV 方向需要:逐字节校验接收数据 */ #if PHASE8_DIRECTION != PHASE8_DIR_SEND -static int phase8_verify(uint8_t *buf, uint32_t size, uint32_t offset) { +static int32_t phase8_verify(uint8_t *buf, uint32_t size, uint32_t offset) { for (uint32_t i = 0; i < size; i++) { if (buf[i] != (uint8_t)((offset + i) & 0xFF)) { DBG_ERROR("Phase8: mismatch at %lu (expect 0x%02X, got 0x%02X)", @@ -476,7 +476,7 @@ static int phase8_verify(uint8_t *buf, uint32_t size, uint32_t offset) { } #endif -static void phase8_run(int sockfd) { +static void phase8_run(int32_t sockfd) { #if PHASE8_DIRECTION == PHASE8_DIR_RECV /* PC 发送文件(尾附 4B CRC32),MCU 接收并逐字节 + CRC32 校验 */ DBG_INFO("Phase8[RECV]: receiving %d-byte file", PHASE8_FILE_SIZE); @@ -484,14 +484,14 @@ static void phase8_run(int sockfd) { uint32_t recvd = 0; uint32_t crc = 0xFFFFFFFFu; while (recvd < PHASE8_FILE_SIZE) { - int to_read = (PHASE8_FILE_SIZE - recvd) > (TEST_BUF_SIZE - 4) + int32_t to_read = (PHASE8_FILE_SIZE - recvd) > (TEST_BUF_SIZE - 4) ? (TEST_BUF_SIZE - 4) : (int)(PHASE8_FILE_SIZE - recvd); - int n = net_recv(sockfd, s_rx_buf, to_read, 0); + int32_t n = net_recv(sockfd, s_rx_buf, to_read, 0); if (n <= 0) { DBG_ERROR("Phase8[RECV]: recv fail at %lu (ret=%d)", recvd, n); return; } - int ok = phase8_verify(s_rx_buf, (uint32_t)n, recvd); + int32_t ok = phase8_verify(s_rx_buf, (uint32_t)n, recvd); TEST_CHECK(ok, "Phase8[RECV]: verify at %lu (%d bytes)", recvd, n); if (!ok) return; crc = phase8_crc32_step(crc, s_rx_buf, (uint32_t)n); @@ -502,7 +502,7 @@ static void phase8_run(int sockfd) { uint8_t foot[4]; uint32_t got = 0; while (got < 4) { - int n = net_recv(sockfd, foot + got, (int)(4 - got), 0); + int32_t n = net_recv(sockfd, foot + got, (int)(4 - got), 0); if (n <= 0) { DBG_ERROR("Phase8[RECV]: footer recv fail (ret=%d)", n); return; @@ -527,7 +527,7 @@ static void phase8_run(int sockfd) { uint32_t chunk = (PHASE8_FILE_SIZE - sent) > (uint32_t)TEST_BUF_SIZE ? (uint32_t)TEST_BUF_SIZE : (PHASE8_FILE_SIZE - sent); phase8_fill(s_tx_buf, chunk, sent); - int n = net_send(sockfd, s_tx_buf, (int)chunk, 0); + int32_t n = net_send(sockfd, s_tx_buf, (int)chunk, 0); if (n <= 0) { DBG_ERROR("Phase8[SEND]: send fail at %lu (ret=%d)", sent, n); return; @@ -547,7 +547,7 @@ static void phase8_run(int sockfd) { uint32_t chunk = (PHASE8_FILE_SIZE - sent) > (uint32_t)TEST_BUF_SIZE ? (uint32_t)TEST_BUF_SIZE : (PHASE8_FILE_SIZE - sent); phase8_fill(s_tx_buf, chunk, sent); - int n = net_send(sockfd, s_tx_buf, (int)chunk, 0); + int32_t n = net_send(sockfd, s_tx_buf, (int)chunk, 0); if (n <= 0) { DBG_ERROR("Phase8[ECHO]: send fail at %lu (ret=%d)", sent, n); return; @@ -557,12 +557,12 @@ static void phase8_run(int sockfd) { uint32_t recvd = 0; while (recvd < PHASE8_FILE_SIZE) { - int n = net_recv(sockfd, s_rx_buf, TEST_BUF_SIZE, 0); + int32_t n = net_recv(sockfd, s_rx_buf, TEST_BUF_SIZE, 0); if (n <= 0) { DBG_ERROR("Phase8[ECHO]: recv fail at %lu (ret=%d)", recvd, n); return; } - int ok = phase8_verify(s_rx_buf, (uint32_t)n, recvd); + int32_t ok = phase8_verify(s_rx_buf, (uint32_t)n, recvd); TEST_CHECK(ok, "Phase8[ECHO]: verify at %lu (%d bytes)", recvd, n); if (!ok) return; recvd += (uint32_t)n; @@ -595,7 +595,7 @@ static void phase8_run(int sockfd) { #include "net_select.h" static uint32_t s_phase9_count = 0; -static void phase9_run(int sockfd) { +static void phase9_run(int32_t sockfd) { net_fd_set readfds; net_timeval tv; @@ -609,14 +609,14 @@ static void phase9_run(int sockfd) { tv.tv_sec = 0; tv.tv_usec = 50000; - int nfds = net_select(sockfd + 1, &readfds, NULL, NULL, &tv); + int32_t nfds = net_select(sockfd + 1, &readfds, NULL, NULL, &tv); if (nfds <= 0) return; /* Socket 0 可读:非阻塞接收并回显 */ if (NET_FD_ISSET(sockfd, &readfds)) { - int n = net_recv(sockfd, s_rx_buf, TEST_BUF_SIZE, NET_MSG_DONTWAIT); + int32_t n = net_recv(sockfd, s_rx_buf, TEST_BUF_SIZE, NET_MSG_DONTWAIT); if (n > 0) { - int sent = net_send(sockfd, s_rx_buf, n, 0); + int32_t sent = net_send(sockfd, s_rx_buf, n, 0); if (sent == n) { s_phase9_count++; TEST_CHECK(1, "Phase9: select echo #%lu (%d bytes)", s_phase9_count, n); @@ -645,7 +645,7 @@ static void phase9_run(int sockfd) { * =================================================================== */ #ifdef ENABLE_PHASE10_TESTS -static int s_phase10_stage = 0; +static int32_t s_phase10_stage = 0; static uint32_t s_phase10_recv = 0; static void phase10_fill(uint8_t *buf, uint32_t size, uint8_t seed) { @@ -654,19 +654,19 @@ static void phase10_fill(uint8_t *buf, uint32_t size, uint8_t seed) { } } -static int phase10_verify(uint8_t *buf, uint32_t size, uint8_t seed) { +static int32_t phase10_verify(uint8_t *buf, uint32_t size, uint8_t seed) { for (uint32_t i = 0; i < size; i++) { if (buf[i] != (uint8_t)(seed + i)) return 0; } return 1; } -static void phase10_run(int sockfd) { +static void phase10_run(int32_t sockfd) { /* 阶段 0 → 1:发送 4095B */ if (s_phase10_stage == 0) { DBG_INFO("Phase10: 4095B (DMA-1)"); phase10_fill(s_tx_buf, 4095, 0x10); - int sent = net_send(sockfd, s_tx_buf, 4095, 0); + int32_t sent = net_send(sockfd, s_tx_buf, 4095, 0); TEST_CHECK(sent == 4095, "Phase10: send 4095B (sent=%d)", sent); s_phase10_recv = 0; s_phase10_stage = 1; @@ -674,7 +674,7 @@ static void phase10_run(int sockfd) { } /* 阶段 1:接收并验证 4095B 回显 */ if (s_phase10_stage == 1) { - int n = net_recv(sockfd, s_rx_buf + s_phase10_recv, (int)(4095 - s_phase10_recv), 0); + int32_t n = net_recv(sockfd, s_rx_buf + s_phase10_recv, (int)(4095 - s_phase10_recv), 0); if (n > 0) { s_phase10_recv += (uint32_t)n; if (s_phase10_recv >= 4095) { @@ -688,7 +688,7 @@ static void phase10_run(int sockfd) { if (s_phase10_stage == 2) { DBG_INFO("Phase10: 4096B (DMA exact)"); phase10_fill(s_tx_buf, 4096, 0x20); - int sent = net_send(sockfd, s_tx_buf, 4096, 0); + int32_t sent = net_send(sockfd, s_tx_buf, 4096, 0); TEST_CHECK(sent == 4096, "Phase10: send 4096B (sent=%d)", sent); s_phase10_recv = 0; s_phase10_stage = 3; @@ -696,7 +696,7 @@ static void phase10_run(int sockfd) { } /* 阶段 3:接收并验证 4096B 回显 */ if (s_phase10_stage == 3) { - int n = net_recv(sockfd, s_rx_buf + s_phase10_recv, (int)(4096 - s_phase10_recv), 0); + int32_t n = net_recv(sockfd, s_rx_buf + s_phase10_recv, (int)(4096 - s_phase10_recv), 0); if (n > 0) { s_phase10_recv += (uint32_t)n; if (s_phase10_recv >= 4096) { @@ -710,7 +710,7 @@ static void phase10_run(int sockfd) { if (s_phase10_stage == 4) { DBG_INFO("Phase10: 4097B (DMA+1, 2 SPI xacts)"); phase10_fill(s_tx_buf, 4097, 0x30); - int sent = net_send(sockfd, s_tx_buf, 4097, 0); + int32_t sent = net_send(sockfd, s_tx_buf, 4097, 0); TEST_CHECK(sent == 4097, "Phase10: send 4097B (sent=%d)", sent); s_phase10_recv = 0; s_phase10_stage = 5; @@ -718,7 +718,7 @@ static void phase10_run(int sockfd) { } /* 阶段 5:接收并验证 4097B 回显 */ if (s_phase10_stage == 5) { - int n = net_recv(sockfd, s_rx_buf + s_phase10_recv, (int)(4097 - s_phase10_recv), 0); + int32_t n = net_recv(sockfd, s_rx_buf + s_phase10_recv, (int)(4097 - s_phase10_recv), 0); if (n > 0) { s_phase10_recv += (uint32_t)n; if (s_phase10_recv >= 4097) { @@ -731,7 +731,7 @@ static void phase10_run(int sockfd) { /* 阶段 6:零长度发送 —— 期望 net_send 返回 -1(NET_ERR_INVAL) */ if (s_phase10_stage == 6) { DBG_INFO("Phase10: zero-length send"); - int sent = net_send(sockfd, s_tx_buf, 0, 0); + int32_t sent = net_send(sockfd, s_tx_buf, 0, 0); TEST_CHECK(sent < 0, "Phase10: send 0B returns %d (expect <0)", sent); s_phase10_stage = 7; return; @@ -740,8 +740,8 @@ static void phase10_run(int sockfd) { if (s_phase10_stage == 7) { DBG_INFO("Phase10: 20x rapid sends (100B each)"); phase10_fill(s_tx_buf, 100, 0x40); - int all_ok = 1; - for (int i = 0; i < 20; i++) { + int32_t all_ok = 1; + for (int32_t i = 0; i < 20; i++) { if (net_send(sockfd, s_tx_buf, 100, 0) != 100) { TEST_CHECK(0, "Phase10: rapid #%d fail", i + 1); all_ok = 0; break; @@ -759,7 +759,7 @@ static void phase10_run(int sockfd) { } /* 阶段 8:接收 2000B 回显总和并验证 */ if (s_phase10_stage == 8) { - int n = net_recv(sockfd, s_rx_buf + s_phase10_recv, (int)(2000 - s_phase10_recv), 0); + int32_t n = net_recv(sockfd, s_rx_buf + s_phase10_recv, (int)(2000 - s_phase10_recv), 0); if (n > 0) { s_phase10_recv += (uint32_t)n; if (s_phase10_recv >= 2000) { @@ -787,10 +787,10 @@ static void phase10_run(int sockfd) { * =================================================================== */ #ifdef ENABLE_PHASE11_TESTS -static int s_phase11_armed = 0; +static int32_t s_phase11_armed = 0; static uint32_t s_phase11_start = 0; -static void phase11_run(int sockfd) { +static void phase11_run(int32_t sockfd) { /* * 第一次调用:记录开始时间并 arm。 * 后续调用:非阻塞 recv,若返回 0 表示连接已断开(KeepAlive 触发)。 @@ -803,7 +803,7 @@ static void phase11_run(int sockfd) { return; } - int n = net_recv(sockfd, s_rx_buf, 64, NET_MSG_DONTWAIT); + int32_t n = net_recv(sockfd, s_rx_buf, 64, NET_MSG_DONTWAIT); if (n == 0) { uint32_t elapsed = HAL_GetTick() - s_phase11_start; TEST_CHECK(elapsed >= 60000, @@ -816,11 +816,11 @@ static void phase11_run(int sockfd) { void net_layer_test_main(void) { - int sockfd = -1; - int phase_active = 0; + int32_t sockfd = -1; + int32_t phase_active = 0; uint32_t accept_retry = 0; struct net_sockaddr_in client_addr; - int addrlen = sizeof(client_addr); + int32_t addrlen = sizeof(client_addr); sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM, 0); if (sockfd < 0) { @@ -855,7 +855,7 @@ void net_layer_test_main(void) */ for (;;) { if (!phase_active) { - int acc = net_accept(sockfd, (struct net_sockaddr *)&client_addr, &addrlen); + int32_t acc = net_accept(sockfd, (struct net_sockaddr *)&client_addr, &addrlen); if (acc < 0) { if (accept_retry % 100 == 0) { DBG_INFO("Test task: waiting for connection..."); diff --git a/test/storage_test.h b/test/storage_test.h new file mode 100644 index 0000000..e0d6b94 --- /dev/null +++ b/test/storage_test.h @@ -0,0 +1,57 @@ +/* + * 模块名称:Storage Test Suite + * 模块功能:FTL + FatFS 存储测试的声明与断言宏。实现见 storage_test_task.c, + * 统一由 ch395fTestTask 调度;正常产品构建下由 defaultTask 调用, + * 行为与原 defaultTask 内联逻辑一致。 + * 适用平台:STM32F4 系列 + * 作者:王建锋 + * 创建日期:2026-08-26 + */ + +#ifndef __STORAGE_TEST_H +#define __STORAGE_TEST_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef struct { + uint16_t total; + uint16_t passed; + uint16_t failed; +} storage_test_stats_t; + +extern storage_test_stats_t g_storage_stats; + +#define STORAGE_TEST_CHECK(cond, fmt, ...) do { \ + g_storage_stats.total++; \ + if (cond) { \ + g_storage_stats.passed++; \ + DBG_INFO(" [PASS] " fmt, ##__VA_ARGS__); \ + } else { \ + g_storage_stats.failed++; \ + DBG_ERROR(" [FAIL] " fmt, ##__VA_ARGS__); \ + } \ +} while (0) + +#define STORAGE_TEST_REPORT(name) do { \ + DBG_INFO("=== %s: %d/%d PASSED (failed=%d) ===", \ + name, g_storage_stats.passed, g_storage_stats.total, \ + g_storage_stats.failed); \ +} while (0) + +/* 存储测试入口:FTL + FatFS 文件系统与性能测试 */ +void storage_test_run(void); + +/* 存储套件恢复测试(默认关闭):取消注释并编译 STORAGE 套件运行一次, + * 即可重建 BBT(清除 TC-STO-03 等注入的坏块)并重新格式化,恢复设备干净态。 + * 恢复完成后建议重新注释并编译正常构建。 */ +//#define ENABLE_STORAGE_RECOVERY_TESTS + +#ifdef __cplusplus +} +#endif + +#endif /* __STORAGE_TEST_H */ diff --git a/test/storage_test_task.c b/test/storage_test_task.c new file mode 100644 index 0000000..373a79c --- /dev/null +++ b/test/storage_test_task.c @@ -0,0 +1,384 @@ +/****************************************************************************** + * 模块名称:Storage Test Task + * 模块功能:FTL + FatFS 存储测试,从 defaultTask 迁入,统一由 ch395fTestTask + * 调度运行。正常产品构建下由 defaultTask 调用,行为与原内联逻辑一致。 + * 作者:王建锋 + * 创建日期:2026-08-26 + ******************************************************************************/ + +#include +#include "main.h" +#include "ff.h" + +#define DBG_TAG "[STORAGE-TEST]" +#include "dbg_log.h" +#include "storage_test.h" +#include "nand_ftl.h" +#include "diskio.h" +#include "gd5f2gq5ue.h" + +storage_test_stats_t g_storage_stats; + +static uint8_t s_perf_buf[4096]; + +/* TC-STO-01: FTL 掉电恢复(journal 持久化) + * 写文件并同步落盘,随后模拟掉电(卸载 + 复位 FTL + 重新 resume),再挂载校验内容一致。 + */ +static void storage_test_powerloss(void) +{ + static FATFS fs; + static FIL fil; + const uint32_t len = 4096; + uint8_t wbuf[512]; + uint8_t rbuf[512]; + UINT bw, br; + uint32_t i; + int ok = 0; + + DBG_INFO("=== Storage Test: TC-STO-01 Power-loss recovery ==="); + + if (f_mount(&fs, "", 1) != FR_OK) { + STORAGE_TEST_CHECK(0, "TC-STO-01 mount"); + return; + } + + /* Phase 1: 写已知内容文件并同步 */ + for (i = 0; i < sizeof(wbuf); i++) { + wbuf[i] = (uint8_t)((i * 31 + 7) & 0xFF); + } + if (f_open(&fil, "pl.dat", FA_CREATE_ALWAYS | FA_WRITE) == FR_OK) { + for (i = 0; i < len; i += sizeof(wbuf)) { + f_write(&fil, wbuf, sizeof(wbuf), &bw); + } + f_sync(&fil); + f_close(&fil); + } else { + STORAGE_TEST_CHECK(0, "TC-STO-01 f_open(write)"); + return; + } + + /* 模拟掉电:卸载 + 复位 FTL + 重新 resume */ + f_mount(&fs, "", 0); + nand_ftl_deinit(); + nand_ftl_init(); + + if (f_mount(&fs, "", 1) != FR_OK) { + STORAGE_TEST_CHECK(0, "TC-STO-01 remount after resume"); + return; + } + + /* Phase 2: 重新打开校验 */ + if (f_open(&fil, "pl.dat", FA_READ) == FR_OK) { + ok = 1; + for (i = 0; i < len; i += sizeof(rbuf)) { + f_read(&fil, rbuf, sizeof(rbuf), &br); + for (uint32_t j = 0; j < br; j++) { + if (rbuf[j] != (uint8_t)(((i + j) * 31 + 7) & 0xFF)) { + ok = 0; + break; + } + } + if (!ok) break; + } + f_close(&fil); + } else { + ok = 0; + } + STORAGE_TEST_CHECK(ok, "TC-STO-01 content survived power-loss (resume)"); + f_unlink("pl.dat"); +} + +/* TC-STO-02: 大文件 / 多扇区 / 随机 seek + * 写 256KB 位置相关模式文件,顺序读回比对 + 随机偏移 seek 读比对。 + */ +static void storage_test_largefile(void) +{ + static FATFS fs; + static FIL fil; + const uint32_t total = 256 * 1024; + const uint32_t chunk = 4096; + uint8_t buf[4096]; + UINT bw, br; + uint32_t off, i; + int ok = 1; + + DBG_INFO("=== Storage Test: TC-STO-02 Large file / random seek ==="); + + if (f_mount(&fs, "", 1) != FR_OK) { + STORAGE_TEST_CHECK(0, "TC-STO-02 mount"); + return; + } + + /* 写:位置相关模式 */ + if (f_open(&fil, "big.dat", FA_CREATE_ALWAYS | FA_WRITE) != FR_OK) { + STORAGE_TEST_CHECK(0, "TC-STO-02 f_open(write)"); + return; + } + for (off = 0; off < total; off += chunk) { + for (i = 0; i < chunk; i++) { + buf[i] = (uint8_t)(((off + i) * 31 + 7) & 0xFF); + } + f_write(&fil, buf, chunk, &bw); + } + f_close(&fil); + + /* 顺序读回 */ + if (f_open(&fil, "big.dat", FA_READ) != FR_OK) { + STORAGE_TEST_CHECK(0, "TC-STO-02 f_open(read)"); + return; + } + for (off = 0; off < total; off += chunk) { + f_read(&fil, buf, chunk, &br); + if (br != chunk) { ok = 0; break; } + for (i = 0; i < chunk; i++) { + if (buf[i] != (uint8_t)(((off + i) * 31 + 7) & 0xFF)) { ok = 0; break; } + } + if (!ok) break; + } + STORAGE_TEST_CHECK(ok, "TC-STO-02 sequential 256KB read consistent"); + + /* 随机 seek 读 */ + if (ok) { + uint32_t seek_offs[] = { 0, 33333, 131072, 200000, total - 200 }; + int seek_ok = 1; + for (int s = 0; s < 5; s++) { + uint32_t so = seek_offs[s]; + uint32_t sc = 200; + if (f_lseek(&fil, so) != FR_OK) { seek_ok = 0; break; } + f_read(&fil, buf, sc, &br); + for (i = 0; i < sc; i++) { + if (buf[i] != (uint8_t)(((so + i) * 31 + 7) & 0xFF)) { seek_ok = 0; break; } + } + if (!seek_ok) break; + } + STORAGE_TEST_CHECK(seek_ok, "TC-STO-02 random seek read consistent"); + } + f_close(&fil); + f_unlink("big.dat"); +} + +/* TC-STO-03: 坏块注入下存储写入 + * 清空旧 map → 注入若干好块为坏(持久化) → 重建文件系统 → 正常写读 → 校验坏块仍被报告。 + */ +static void storage_test_badblock(void) +{ + static FATFS fs; + static FIL fil; + uint8_t wbuf[512], rbuf[512]; + UINT bw, br; + uint32_t bad[3]; + int nbad = 0; + int ok = 1; + + DBG_INFO("=== Storage Test: TC-STO-03 Bad-block injection during storage ==="); + + /* 清空旧 dhara map,避免后续注入的坏块被旧映射引用 */ + nand_ftl_format(); + + /* 注入:挑出厂好块标记为坏(更新并持久化 BBT) */ + for (uint32_t b = 100; b < gd5f_get_usable_blocks() && nbad < 3; b++) { + if (gd5f2gq5ue_is_block_bad(b) == 0) { + gd5f2gq5ue_mark_block_bad(b); + bad[nbad++] = b; + } + } + if (nbad < 3) { + STORAGE_TEST_CHECK(0, "TC-STO-03 could not find 3 good blocks (got %d)", nbad); + return; + } + STORAGE_TEST_CHECK(nbad == 3, "TC-STO-03 injected %d bad blocks", nbad); + + /* 复位 FTL 使 dhara 经 is_block_bad 看到新坏块 */ + f_mount(&fs, "", 0); + nand_ftl_deinit(); + nand_ftl_init(); + + /* 重建文件系统(dhara 仍会跳过坏块) */ + { + MKFS_PARM opts = { FM_FAT32, 0, 0, 0, 0 }; + uint8_t work[FF_MAX_SS]; + if (f_mkfs("", &opts, work, sizeof(work)) != FR_OK) { + STORAGE_TEST_CHECK(0, "TC-STO-03 f_mkfs"); + return; + } + } + if (f_mount(&fs, "", 1) != FR_OK) { + STORAGE_TEST_CHECK(0, "TC-STO-03 mount"); + return; + } + + /* 正常文件写读 */ + for (uint32_t i = 0; i < sizeof(wbuf); i++) { + wbuf[i] = (uint8_t)((i * 17 + 3) & 0xFF); + } + if (f_open(&fil, "bbt.dat", FA_CREATE_ALWAYS | FA_WRITE) == FR_OK) { + f_write(&fil, wbuf, sizeof(wbuf), &bw); + f_close(&fil); + } else { + STORAGE_TEST_CHECK(0, "TC-STO-03 f_open(write)"); + return; + } + + if (f_open(&fil, "bbt.dat", FA_READ) == FR_OK) { + f_read(&fil, rbuf, sizeof(rbuf), &br); + f_close(&fil); + if (br == sizeof(wbuf)) { + for (uint32_t i = 0; i < sizeof(rbuf); i++) { + if (rbuf[i] != wbuf[i]) { ok = 0; break; } + } + } else { + ok = 0; + } + } else { + ok = 0; + } + STORAGE_TEST_CHECK(ok, "TC-STO-03 file intact with injected bad blocks"); + + /* 校验注入坏块仍被报告为坏(未被 dhara 占用) */ + int badok = 1; + for (int k = 0; k < nbad; k++) { + if (gd5f2gq5ue_is_block_bad(bad[k]) != 1) { badok = 0; } + } + STORAGE_TEST_CHECK(badok, "TC-STO-03 injected blocks still reported bad"); + + f_unlink("bbt.dat"); + /* 注意:注入的坏块已持久化,测试设备专用;如需恢复需重建 BBT / 重新格式化 */ +} + +#ifdef ENABLE_STORAGE_RECOVERY_TESTS +static void storage_test_recover(void); +#endif + +void storage_test_run(void) +{ + static FATFS fs; + static FIL fil; + static uint8_t work[FF_MAX_SS]; + char read_buf[64]; + const char *test_str = "Hello FatFS + dhara FTL!"; + + memset(&g_storage_stats, 0, sizeof(g_storage_stats)); + + /* ==================== FTL + FatFS 文件系统测试 ==================== */ + + FRESULT res = f_mount(&fs, "", 1); + if (res == FR_NO_FILESYSTEM) { + MKFS_PARM opts = { FM_FAT32, 0, 0, 0, 0 }; + res = f_mkfs("", &opts, work, sizeof(work)); + STORAGE_TEST_CHECK(res == FR_OK, "f_mkfs"); + res = f_mount(&fs, "", 1); + } + STORAGE_TEST_CHECK(res == FR_OK, "f_mount"); + + if (res == FR_OK) { + UINT bw, br; + + res = f_open(&fil, "test.txt", FA_CREATE_ALWAYS | FA_WRITE); + STORAGE_TEST_CHECK(res == FR_OK, "f_open(write)"); + if (res == FR_OK) { + f_write(&fil, test_str, (UINT)strlen(test_str), &bw); + STORAGE_TEST_CHECK(bw == (UINT)strlen(test_str), "f_write (%u bytes)", bw); + f_close(&fil); + + res = f_open(&fil, "test.txt", FA_READ); + STORAGE_TEST_CHECK(res == FR_OK, "f_open(read)"); + if (res == FR_OK) { + f_read(&fil, read_buf, (UINT)(sizeof(read_buf) - 1), &br); + f_close(&fil); + read_buf[br] = '\0'; + STORAGE_TEST_CHECK(br == (UINT)strlen(test_str) && + memcmp(test_str, read_buf, br) == 0, + "data verify \"%s\"", read_buf); + } + } + f_unlink("test.txt"); + } + + /* ==================== FTL + FatFS 性能测试 ==================== */ + + DBG_INFO("=== Storage Test: Performance ==="); + + { + FIL pfile; + UINT pw, pr; + uint32_t t0, tw, tr; + uint32_t i; + uint32_t total_kb = 128; + uint32_t chunk = sizeof(s_perf_buf); + uint32_t count = (total_kb * 1024) / chunk; + + memset(s_perf_buf, 0xA5, sizeof(s_perf_buf)); + + /* 写入 */ + t0 = HAL_GetTick(); + if (f_open(&pfile, "perf.dat", FA_CREATE_ALWAYS | FA_WRITE) == FR_OK) { + for (i = 0; i < count; i++) { + f_write(&pfile, s_perf_buf, chunk, &pw); + } + f_close(&pfile); + } + tw = HAL_GetTick() - t0; + + /* 读取 */ + t0 = HAL_GetTick(); + if (f_open(&pfile, "perf.dat", FA_READ) == FR_OK) { + for (i = 0; i < count; i++) { + f_read(&pfile, s_perf_buf, chunk, &pr); + } + f_close(&pfile); + } + tr = HAL_GetTick() - t0; + + f_unlink("perf.dat"); + + if (tw > 0) { + DBG_INFO(" Write: %lu KB in %lu ms = %lu KB/s", + total_kb, tw, (total_kb * 1000) / tw); + } + if (tr > 0) { + DBG_INFO(" Read: %lu KB in %lu ms = %lu KB/s", + total_kb, tr, (total_kb * 1000) / tr); + } + } + + /* ==================== 存储集成测试(TC-STO-01~03) ==================== */ + + storage_test_powerloss(); + storage_test_largefile(); + storage_test_badblock(); + +#ifdef ENABLE_STORAGE_RECOVERY_TESTS + storage_test_recover(); +#endif + + /* ==================== 汇总报告 ==================== */ + + STORAGE_TEST_REPORT("Storage Tests"); +} + +#ifdef ENABLE_STORAGE_RECOVERY_TESTS +/* TC-STO-04: 重建 BBT + 重新格式化(恢复 TC-STO-03 注入的坏块) + * 仅在启用 ENABLE_STORAGE_RECOVERY_TESTS 时编译运行;运行一次即可把设备 + * 恢复到干净出厂态(清除运行时注入坏块并清空 dhara map)。 + */ +static void storage_test_recover(void) +{ + DBG_INFO("=== Storage Test: TC-STO-04 BBT rebuild + format (recovery) ==="); + + int r = gd5f2gq5ue_bbt_rebuild(); + STORAGE_TEST_CHECK(r == GD5F_OK, "TC-STO-04 bbt rebuild"); + + nand_ftl_format(); + STORAGE_TEST_CHECK(1, "TC-STO-04 nand_ftl_format done"); + + uint8_t fb[GD5F_TOTAL_BLOCKS / 8]; + uint32_t ver = 0; + if (gd5f_bbt_dump_flash(fb, &ver) == GD5F_OK) { + STORAGE_TEST_CHECK(ver >= 1, "TC-STO-04 persisted BBT version=%lu", ver); + } else { + STORAGE_TEST_CHECK(0, "TC-STO-04 dump_flash failed"); + } + + DBG_INFO("TC-STO-04 recovery complete; recommend power-cycle to re-init cleanly"); +} +#endif diff --git a/test/test_config.h b/test/test_config.h new file mode 100644 index 0000000..f9f1774 --- /dev/null +++ b/test/test_config.h @@ -0,0 +1,73 @@ +/* + * 模块名称:统一测试套件选择 + * 模块功能:集中选择当前构建要运行的测试套件。所有测试统一由 + * ch395fTestTask(StartCh395fTestTask)调度,defaultTask 在测试 + * 构建下空闲,正常产品构建下仅运行存储挂载/测试。 + * 适用平台:STM32F4 系列 + * 作者:王建锋 + * 创建日期:2026-08-26 + * + * 一次构建仅启用一个 TEST_SUITE_*(或都不启用 = 正常产品构建): + * TEST_SUITE_CH395F : CH395F 测试(沿用 ch395f_test.h 阶段宏,HW 1~5 / NET 6~11) + * TEST_SUITE_STORAGE : FTL + FatFS 存储测试(从 defaultTask 迁入) + * TEST_SUITE_GD5F : GD5F2GQ5UE NAND 驱动测试 + */ + +#ifndef __TEST_CONFIG_H +#define __TEST_CONFIG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* === 选择当前构建的测试套件(三选一,或都注释 = 正常产品构建)=== */ +//#define TEST_SUITE_CH395F +//#define TEST_SUITE_STORAGE +//#define TEST_SUITE_GD5F + +#if defined(TEST_SUITE_CH395F) + defined(TEST_SUITE_STORAGE) + defined(TEST_SUITE_GD5F) > 1 +#error "测试套件互斥:一次构建只能启用一个 TEST_SUITE_*" +#endif + +#if defined(TEST_SUITE_CH395F) || defined(TEST_SUITE_STORAGE) || defined(TEST_SUITE_GD5F) +#define TEST_SUITE_SELECTED +#endif + +#ifdef TEST_SUITE_CH395F +#include "ch395f_test.h" +#define TEST_SUITE_NAME "CH395F" +#endif + +#ifdef TEST_SUITE_STORAGE +#include "storage_test.h" +#define TEST_SUITE_NAME "STORAGE" +#endif + +#ifdef TEST_SUITE_GD5F +/* 选中 GD5F 套件时,自动开启 gd5f 测试总开关与全部阶段 */ +#define ENABLE_GD5F_TESTS +#define ENABLE_GD5F_PHASE1_TESTS +#define ENABLE_GD5F_PHASE2_TESTS +#define ENABLE_GD5F_PHASE3_TESTS +#define ENABLE_GD5F_PHASE4_TESTS +#define ENABLE_GD5F_PHASE5_TESTS +#define ENABLE_GD5F_PHASE6_TESTS +#define ENABLE_GD5F_PHASE7_TESTS +#define ENABLE_GD5F_PHASE8_TESTS +#define ENABLE_GD5F_PHASE9_TESTS +#define ENABLE_GD5F_PHASE10_TESTS +#include "gd5f_test.h" +#define TEST_SUITE_NAME "GD5F" +#endif + +#ifndef TEST_SUITE_NAME +#define TEST_SUITE_NAME "NONE" +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* __TEST_CONFIG_H */