存储测试通过
This commit is contained in:
741
docs/GD5F2GQ5UE_Test_Guide.md
Normal file
741
docs/GD5F2GQ5UE_Test_Guide.md
Normal file
@@ -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()`<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 |
|
||||
|
||||
@@ -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 往往只在“区间贴到设备边界”时才触发,正常情况能过,建议测试覆盖边界块
|
||||
|
||||
|
||||
@@ -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 层可见,多任务读写同一文件需在应用层同步。
|
||||
|
||||
Reference in New Issue
Block a user