Files
STM32F4-Base/docs/STM32F4-Base存储架构说明.md
2026-07-21 22:08:11 +08:00

466 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# STM32F4-Base 存储架构说明
## 1. 概述
本 项目在 GD5F2GQ5UE SPI NAND Flash256MB上实现了三层存储软件栈
```
┌─────────────────────────────────────────────────────────────┐
│ 应用程序 (main, freertos) │
├─────────────────────────────────────────────────────────────┤
│ │
│ FatFs (文件系统) │
│ │
├─────────────────────────────────────────────────────────────┤
│ dhara FTL │
│ (地址映射 · 磨损均衡 · 坏块管理 · 垃圾回收) │
├─────────────────────────────────────────────────────────────┤
│ GD5F2GQ5UE NAND Flash 驱动 (底层 SPI) │
│ 硬件 SPI · 页读写 · 块擦除 · BBT · 内部 ECC 使能 │
├─────────────────────────────────────────────────────────────┤
│ SPI1 硬件外设 (PB3 SCK, PB4 MISO, PB5 MOSI) │
│ 42MHz · Mode 0 · MSB First · DMA2 (S0-RX, S3-TX) │
│ 小包轮询(≤32B) · 页数据 DMA (>32B) │
└─────────────────────────────────────────────────────────────┘
```
```
┌─────────────────────────────────────────────────────────────┐
│ 物理分区布局 (256MB) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ftl_fatfs (256MB) │ │
│ │ Block 0~2047 │ │
│ │ dhara FTL + FatFS全部 2048 个块) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ 偏移: 0 256MB│
└─────────────────────────────────────────────────────────────┘
```
**数据路径:**
- **应用** → `FatFS``dhara FTL``gd5f2gq5ue 驱动`。FTL 提供 LBA 到物理页的映射、磨损均衡、坏块管理和垃圾回收FatFS 通过标准 `disk_*` 接口访问 FTL 提供的块设备。
**初始化顺序:**
```
HAL_Init → SystemClock_Config → MX_GPIO_Init → MX_USART1_UART_Init
→ MX_SPI2_Init → MX_SPI1_Init
→ gd5f2gq5ue_init() // NAND 底层驱动 (含 BBT 扫描 + ECC 使能)
→ app_main_init() // 应用层初始化
→ osKernelStart() // FreeRTOS 启动
├─ defaultTask → f_mount() → 存储测试 + 业务
├─ netTask → 网络协议栈轮询
├─ adcTask → ADC 采集
└─ rs485Task → RS485 通信
```
---
## 2. 硬件规格GD5F2GQ5UE
| 参数 | 值 |
|------|-----|
| 容量 | 256 MB (2 Gb) |
| 页大小 (主区) | 2048 bytes (2 KB) |
| 备校区 (Spare) | 64 bytes/页 |
| 每块页数 | 64 |
| 块大小 | **128 KB** (64 × 2 KB) |
| 总块数 | 2048 |
| 总页面数 | 131072 |
| 内部 ECC | 支持 (每页 8bit ECC) |
| 接口 | SPI (Mode 0, CPOL=0 CPHA=0) |
| 最高时钟 | 42 MHz |
| 读页延迟 | < 60 μs |
| 编程延迟 | < 600 μs |
| 块擦除延迟 | < 3 ms |
**SPI 引脚分配:**
| 信号 | GPIO | 说明 |
|------|------|------|
| CS# | PE0 | 片选 (低有效) |
| SCK | PB3 | SPI1 SCK |
| MISO | PB4 | SPI1 MISO (主入从出) |
| MOSI | PB5 | SPI1 MOSI (主出从入) |
| WP# | PB8 | 写保护 (本驱动恒拉高) |
| HOLD# | PE1 | 保持 (本驱动恒拉高) |
---
## 3. 分区布局
单个分区,配置在 `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c`
```c
#define FTL_FATFS_OFFSET 0 // 偏移 0占用全部 256MB
```
### 3.1 分区对照表
| 分区名 | 偏移 | 大小 | 物理块范围 | 逻辑用途 |
|--------|------|------|-----------|----------|
| `ftl_fatfs` | 0 | 256 MB | Block 0~2047 | dhara FTL + FatFS |
### 3.2 配置联动
- **FTL** — `nand_ftl.c``FTL_START_BLOCK = 0``FTL_NUM_BLOCKS = 2048`
- **FatFS** — 通过 `disk_ioctl(GET_SECTOR_COUNT)` 获取 FTL 管理的扇区数
---
## 4. 驱动层gd5f2gq5ue.c/h
### 4.1 SPI 命令集
| 命令 | 编码 | 用途 |
|------|------|------|
| `WRITE_ENABLE` | `0x06` | 写使能 (每个写/擦除前必须发送) |
| `WRITE_DISABLE` | `0x04` | 写禁止 |
| `GET_FEATURE` | `0x0F` | 读状态/feature 寄存器 |
| `SET_FEATURE` | `0x1F` | 写 feature 寄存器 (前需写使能) |
| `READ_ID` | `0x9F` | 读芯片 ID |
| `PAGE_READ` | `0x13` | 将页数据从存储阵列载入内部缓存 |
| `READ_FROM_CACHE` | `0x0B` | 从内部缓存读取数据 |
| `PROGRAM_LOAD` | `0x02` | 将数据写入内部缓存 |
| `PROGRAM_EXEC` | `0x10` | 将缓存数据编程到存储阵列 |
| `BLOCK_ERASE` | `0xD8` | 块擦除 (128KB) |
| `RESET` | `0xFF` | 芯片复位 |
### 4.2 Feature 寄存器
| 地址 | 名称 | 说明 |
|------|------|------|
| `0xA0` | Protect | 块保护 (写入 `0x00` 解除全部保护) |
| `0xB0` | Feature | ECC 使能 (bit4=1 启用) |
| `0xC0` | Status | 状态标志 (OIP/WEL/E_FAIL/P_FAIL/ECC) |
| `0xD0` | Driver | 驱动强度 |
### 4.3 初始化序列
```
1. RESET (FFh)
└─ 等待 5ms
2. READ_ID (9Fh)
└─ 校验 MID=0xC8, DID=0x52
3. BBT 扫描
└─ 读每块最后一页 (page 63) spare byte 0
└─ 非 0xFF 即出厂坏块,写入 s_bbt[]
4. SET_FEATURE (B0h=10h) — 使能内部 8bit ECC
5. SET_FEATURE (A0h=00h) — 解除全部块保护
```
### 4.4 读写擦除操作
**页读取**(任何字节偏移均可,驱动自动定位到页):
```
PAGE_READ (13h + 3字节行地址) → 等待 OIP 清零 → READ_FROM_CACHE (0Bh + 2字节列地址 + dummy)
```
**页编程**NAND 只能将 bit 从 1→0 翻转,编程前必须擦除):
```
WRITE_ENABLE → PROGRAM_LOAD (02h + 列地址 + 数据) → PROGRAM_EXEC (10h + 行地址) → 等待完成 → 检查 P_FAIL
```
**块擦除**(最小擦除单位 128KB参数是字节地址而非块编号
```
WRITE_ENABLE → BLOCK_ERASE (D8h + 3字节字节地址) → 等待完成 → 检查 E_FAIL
```
### 4.5 DMA 传输策略2026-07-21 新增)
SPI1 使用 DMA2 实现页数据级别的不阻塞传输,配置如下:
| 通道 | DMA | 流 | 通道 | 方向 | 优先级 | 模式 |
|------|-----|----|------|------|--------|------|
| SPI1_RX | DMA2 | Stream 0 | CH3 | 外设→内存 | LOW | NORMAL |
| SPI1_TX | DMA2 | Stream 3 | CH3 | 内存→外设 | LOW | NORMAL |
**传输策略:**
- **小包轮询(≤ 32 bytes**:命令字、地址、状态寄存器等短数据使用 `HAL_SPI_Transmit/Receive` 轮询模式,避免 DMA 初始化开销
- **页数据 DMA> 32 bytes**`READ_FROM_CACHE``PROGRAM_LOAD` 的页数据段使用 `HAL_SPI_Receive_DMA` / `HAL_SPI_Transmit_DMA`
**HAL 内部路由说明:**
`HAL_SPI_Receive_DMA()` 在 2 线 Master 模式内部调用 `HAL_SPI_TransmitReceive_DMA()`,但由于 SPI 状态设置为 `HAL_SPI_STATE_BUSY_RX`HAL 的完成回调为 `HAL_SPI_RxCpltCallback`(而非 `TxRxCpltCallback`),需确保该回调在 `gd5f2gq5ue.c` 中实现。
**完整回调链条:**
| 操作 | 触发回调 | 实现位置 |
|------|---------|---------|
| `PROGRAM_LOAD` TX DMA | `HAL_SPI_TxCpltCallback` | `gd5f2gq5ue.c` |
| `READ_FROM_CACHE` RX DMA | `HAL_SPI_RxCpltCallback` | `gd5f2gq5ue.c` |
| 任意外设错误 | `HAL_SPI_ErrorCallback` | `ch395f.c`(含 SPI1 分支) |
**性能(顺序 128KB 读写FatFS + FTL**
| 模式 | 写 | 读 |
|------|----|----|
| 轮询(改造前) | 330 KB/s | 547 KB/s |
| DMA改造后 | **831 KB/s** | **1855 KB/s** |
| 42MHz SPI 理论极限 | ~5.25 MB/s受 NAND tPROG ≈ 500μs/页 限制) | ~5.25 MB/s |
DMA 消除了轮询模式下 SPI 状态寄存器查检的逐字节 CPU 开销,读写性能分别提升 **2.5×****3.4×**。进一步优化需考虑 Cache Read 模式(重叠 NAND 内部 tR 延迟)或批量编程(减少 tPROG 次数)。
### 4.6 BBT (Bad Block Table)
- 初始化时扫描全部 2048 块最后一页的 spare byte 0
- `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
- `gd5f2gq5ue_print_bbt()` — 打印 BBT 摘要(调试用)
> **注意**BBT 只能在 ECC 禁用时扫描(`gd5f_bbt_scan()`ECC 使能后 spare area 会被 ECC 引擎干扰导致坏块检测不可靠。运行时坏块只来源于真实擦除/编程失败,不要主动重新扫描。
---
## 5. dhara FTL (Flash Translation Layer)
### 5.1 概述
FTL 是 NAND Flash 上方最重要的组件,功能包括:
| 功能 | 说明 |
|------|------|
| **地址映射** | 将 FatFS 的逻辑扇区号 (LBA) 映射到 NAND 物理页 |
| **磨损均衡** | 均匀分布擦除次数,延长芯片寿命 |
| **垃圾回收** | 回收无效页面空间 (GC) |
| **坏块管理** | 擦除/编程失败时自动跳过并标记坏块 |
| **ECC 处理** | 检测和上报硬件 ECC 不可纠正错误 |
### 5.2 数据结构
```c
struct dhara_nand {
int log2_page_size; // 页大小对数 (2KB → 11)
int log2_ppb; // 每块页数对数 (64 → 6)
int num_blocks; // 管理的物理块数 (1024)
};
struct dhara_map {
// 内部状态GC 队列、journal、映射表等
};
```
### 5.3 NAND HAL (nand_ftl.c)
dhara 通过 7 个回调函数访问物理 NAND
| 回调 | 功能 | 对应底层函数 |
|------|------|-------------|
| `dhara_nand_is_bad` | 查询坏块 | `gd5f2gq5ue_is_block_bad(block + 1024)` |
| `dhara_nand_mark_bad` | 标记坏块 | `gd5f2gq5ue_mark_block_bad(block + 1024)` |
| `dhara_nand_erase` | 擦除块 | `nand_block_erase(block + 1024)` |
| `dhara_nand_prog` | 写页 | `nand_program_load + nand_program_exec` |
| `dhara_nand_read` | 读页 | `nand_page_read_to_cache + nand_read_from_cache` |
| `dhara_nand_is_free` | 检查页空闲 | 读前 64 字节判断全为 0xFF |
| `dhara_nand_copy` | 页拷贝 (GC 用) | read + prog 组合 |
所有回调自动将 dhara 逻辑块/页加 `FTL_START_BLOCK` 偏移转换为物理地址。
### 5.4 初始化流程 (disk_initialize)
```
1. 设置 nand 参数 (log2_page_size=11, log2_ppb=6, num_blocks=1024)
2. dhara_map_init(&s_map, &s_nand, s_page_buf, 4)
├─ s_page_buf: dhara 内部使用的 2KB 工作缓冲区
└─ 4: journal 页面数量 (影响 GC 效率, 增大可减少写入放大)
3. dhara_map_resume(&s_map)
├─ 成功: 加载已有映射表
└─ 失败: dhara_map_clear 创建空映射表
4. 页面缓存初始化 (s_cache_buf, s_cached_lpn, s_cache_dirty)
```
### 5.5 页面缓存策略
FTL 之上还有一个 **单页写回缓存 (write-back cache)**
- **读命中**: 直接返回 s_cache_buf 数据
- **读未命中**: 刷出脏页 → 读新页到缓存
- **写**: 写入缓存 → 标记脏
- **全页写入**: 直接刷出 (跳过缓存)
- **同步 (CTRL_SYNC)**: 刷出脏页 + dhara_map_sync
---
## 6. FatFS 集成
### 6.1 配置 (ffconf.h)
```c
#define FF_FS_READONLY 0 // 读写模式
#define FF_USE_MKFS 1 // 启用格式化
#define FF_MIN_SS 512 // 最小扇区大小
#define FF_MAX_SS 512 // 最大扇区大小
#define FF_VOLUMES 1 // 单卷
#define FF_FS_TINY 0 // 非 tiny 模式
#define FF_FS_NORTC 1 // 无 RTC (固定时间戳)
```
### 6.2 disk I/O 接口
| 函数 | 功能 | 关键实现 |
|------|------|----------|
| `disk_initialize` | 初始化 FTL | 见 7.4 节 |
| `disk_status` | 查询状态 | 返回初始化状态 |
| `disk_read` | 读扇区 | 通过 FTL 映射读物理页 |
| `disk_write` | 写扇区 | 通过 FTL 映射写 (缓存优化) |
| `disk_ioctl` | 控制命令 | GET_SECTOR_COUNT/SIZE/BLOCK_SIZE + CTRL_SYNC |
### 6.3 容量计算
```
FTL 管理块数 = 2048 (Block 0~2047)
每块页数 = 64
每页扇区数 (512B) = 4
总扇区数 = 2048 × 64 × 4 = 524288
总容量 = 524288 × 512 = 256MB (原始容量)
FTL 开销后 ≈ 186 MB (随 GC 和 journal 使用量波动)
```
---
## 7. 完整数据流
### 7.1 写文件流程
```
f_write("hello.txt")
└─ FatFS: 计算 LBA, 写扇区
└─ disk_write(0, data, sector=100, count=2)
├─ 计算 LPN = sector / 4 = 25
├─ 缓存未命中 → ftl_flush_cache() → ftl_read_page(25)
├─ 拷贝数据到 s_cache_buf → 标记脏
└─ 全页面写入 → ftl_flush_cache()
└─ dhara_map_write(&s_map, 25, s_cache_buf)
├─ 查找页映射 (或分配新页)
├─ dhara_nand_prog(pg, data) → 物理写
└─ 更新映射表
f_close → disk_ioctl(CTRL_SYNC)
└─ ftl_flush_cache() → dhara_map_sync()
└─ 写 journal 到 NAND (持久化映射表)
```
### 7.2 f_mkfs 格式化流程
```
f_mkfs("", &opts, work, size)
├─ disk_initialize(0) → FTL 初始化
├─ disk_write: 写入引导扇区 (MBR/PBR)
├─ disk_write: 写入 FAT 表
├─ disk_write: 创建根目录
└─ disk_ioctl(CTRL_SYNC) → FTL sync
```
---
## 8. API 参考
### 8.1 底层 NAND 驱动 (gd5f2gq5ue.h)
| 函数 | 说明 |
|------|------|
| `gd5f2gq5ue_init()` | 初始化 NAND (BBT 扫描 + ECC 使能 + 块保护解除) |
| `gd5f2gq5ue_read_id(p_mid, p_did)` | 读芯片 ID |
| `gd5f2gq5ue_read(offset, buf, size)` | 读数据 (支持跨页) |
| `gd5f2gq5ue_write(offset, buf, size)` | 写数据 (支持跨页) |
| `gd5f2gq5ue_erase(offset, size)` | 块擦除 (需块对齐) |
| `gd5f2gq5ue_reset()` | 复位芯片 |
| `gd5f2gq5ue_is_block_bad(block)` | 查询坏块 |
| `gd5f2gq5ue_mark_block_bad(block)` | 标记坏块 |
| `gd5f2gq5ue_bbt_clear()` | 清空 BBT |
| `gd5f2gq5ue_bbt_rescan()` | 重新扫描 BBT |
| `gd5f2gq5ue_print_bbt()` | 打印 BBT |
**SPI 原语FTL 和测试共享):**
| 函数 | 说明 |
|------|------|
| `gd5f_wait_busy(timeout_ms)` | 轮询等待 OIP 清零 |
| `gd5f_write_enable()` | 发送 06h 写使能 |
| `gd5f_read_status(p_status)` | 读状态寄存器 (C0h) |
| `gd5f_page_read(page_addr)` | 页读取 (13h) |
| `gd5f_read_from_cache(column, buf, size)` | 读内部缓存 (0Bh) |
| `gd5f_program_load(column, buf, size)` | PROGRAM LOAD (02h) |
| `gd5f_program_exec(page_addr)` | PROGRAM EXEC (10h) |
| `gd5f_block_erase(block_addr)` | 块擦除 (D8h) |
| `gd5f_check_ecc()` | 检查 ECC 状态位 |
### 8.2 FTL/FatFS (via nand_ftl.c / ff.h)
| 函数 | 说明 |
|------|------|
| `f_mount(&fs, "", 1)` | 挂载文件系统 (首次调用自动初始化 FTL) |
| `f_mkfs("", &opts, work, size)` | 格式化 FAT32 |
| `f_open/f_close/f_read/f_write` | 标准文件操作 |
| `f_unlink("test.txt")` | 删除文件 |
| `disk_initialize(0)` | 显式初始化 FTL (通常由 f_mount 自动调用) |
| `nand_ftl_format()` | 直接格式化 FTL (清空映射表, 慎用) |
---
## 9. 使用示例
### 9.1 FatFS 文件操作
```c
#include "ff.h"
static FATFS fs;
static int fatfs_init(void) {
FRESULT res = f_mount(&fs, "", 1);
if (res == FR_NO_FILESYSTEM) {
/* 首次使用需格式化 */
MKFS_PARM opts = {FM_FAT32, 0, 0, 0, 0};
uint8_t work[512];
res = f_mkfs("", &opts, work, sizeof(work));
if (res != FR_OK) return -1;
res = f_mount(&fs, "", 1);
}
return (res == FR_OK) ? 0 : -1;
}
static void fatfs_write_read(void) {
FIL fil;
UINT bw, br;
const char *msg = "Hello Storage!";
char buf[32];
/* 写文件 */
f_open(&fil, "data.txt", FA_CREATE_ALWAYS | FA_WRITE);
f_write(&fil, msg, strlen(msg), &bw);
f_close(&fil);
/* 读文件 */
f_open(&fil, "data.txt", FA_READ);
f_read(&fil, buf, sizeof(buf), &br);
buf[br] = '\0';
f_close(&fil);
}
```
### 9.2 FTL 格式化
```c
#include "nand_ftl.h"
/* 注意: 此操作将清空 FTL 分区全部数据, 慎用 */
if (nand_ftl_format() == 0) {
DBG_INFO("FTL formatted");
}
```
---
## 10. 注意事项
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 中更新,下次复位后重新扫描出厂坏块并叠加运行时坏块。
5. **功耗** — 擦除操作最大耗时约 5ms (驱动超时设为 5s),页编程约 600ms (超时 1s),读写操作快。在低功耗场景需注意合理安排操作时序。
6. **缓存一致性** — 单页缓存 (s_cache_buf) 仅对 FatFS 层可见,多任务读写同一文件需在应用层同步。