# 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()`<br>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<br>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`）<br>2. `gd5f2gq5ue_write(0, buf, 2048)`<br>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` 目标块<br>2. `write` 已知模式（如 0xAA 填充/递增）<br>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` 落盘<br>2. 模拟掉电：`f_mount("",0)` 卸载 → `nand_ftl_deinit()` 复位 FTL → `nand_ftl_init()` 重新走 `dhara_map_resume`<br>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`，每字节按位置相关模式填充（便于任意偏移校验）<br>2. 顺序读回全量比对<br>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）<br>2. `nand_ftl_deinit()` + `nand_ftl_init()` 使 dhara 经 `is_block_bad` 看到新坏块<br>3. `f_mkfs` 重建文件系统 → 正常文件写读 `bbt.dat` 并校验<br>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` 持久化，覆盖注入坏块）<br>2. `nand_ftl_format()` 清空 dhara map<br>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 |

