# STM32F4-Base 存储架构说明 ## 1. 概述 本 项目在 GD5F2GQ5UE SPI NAND Flash(256MB)上实现了三层存储软件栈: ``` ┌─────────────────────────────────────────────────────────────┐ │ 应用程序 (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) │ │ │ │ ┌─────────────────────────────────────┬───────────────────┐ │ │ │ 预留 (128MB) │ ftl_fatfs (128MB) │ │ │ │ Block 0~1023 │ Block 1024~2047 │ │ │ │ 未使用 │ dhara FTL + FatFS │ │ │ └─────────────────────────────────────┴───────────────────┘ │ │ 偏移: 0 128MB 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 (128U * 1024U * 1024U) // 128MB #define FTL_FATFS_SIZE (128 * 1024 * 1024) // 128MB ``` ### 3.1 分区对照表 | 分区名 | 偏移 | 大小 | 物理块范围 | 逻辑用途 | |--------|------|------|-----------|----------| | 预留 | 0 | 128 MB | Block 0~1023 | 未使用 | | `ftl_fatfs` | 128 MB | 128 MB | Block 1024~2047 | dhara FTL + FatFS | ### 3.2 配置联动 - **FTL** — `nand_ftl.c` 中 `FTL_START_BLOCK = FTL_FATFS_OFFSET / GD5F_BLOCK_SIZE = 1024`,`FTL_NUM_BLOCKS` 自动适配剩余块数 - **FatFS** — 通过 `disk_ioctl(GET_SECTOR_COUNT)` 获取 FTL 管理的扇区数 > 调整分区大小时只需修改 `nand_ftl.c` 中的 `FTL_FATFS_OFFSET` 宏,下游模块自动适配。 --- ## 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 管理块数 = 1024 (Block 1024~2047) 每块页数 = 64 每页扇区数 (512B) = 4 总扇区数 = 1024 × 64 × 4 = 262144 总容量 = 262144 × 512 = 128MB (原始容量) FTL 开销后 ≈ 93 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 层可见,多任务读写同一文件需在应用层同步。