Files
STM32F4-Base/docs/架构设计/STM32F4-Base存储架构说明.md
T
wandering1 cea2b51efb docs(架构): 全景/启动顺序/存储/引用图与 README 同步任务面与 NET 内部模块
- 项目架构总览 v1.1:§1.1 去重复⑥行、硬件行 UART4/TIM3/ADC1;§2.1
  任务表 6→9 行且栈值按 freertos.c 校准;§3 补采集算法链与 TIM3 节拍;
  §5 补 net_events/net_listener/net_pending 与 iec104_serv,单客户端
  恢复窗更正为实测 p50 208ms;§7 目录树、§9 索引、§10 变更记录同步
- 项目启动顺序管理:任务表 5→10 行;启动轨迹表 8→14 行(fw build、
  ADC input layer、phasor/freq/protection、SD2506 明细、TIM3 启动)
- 存储架构说明:§1 初始化顺序块重写(gd5f 归位 app_main_init 内、
  rs485Task 未创建、补 TIM3 与 L1~L3 链)
- 引用关系图说明 §7 统计与产物重跑刷新;就绪屏障 SVG 补 iec104Task
  消费者后重生成(1400x912)
- README:外设引脚纠错(RS485=UART4/PC10/PC11/PA15、TPAFE 控制脚
  PC8/PC9/PC7)、启动流程补 sys_clock/adc_input/TIM3、任务表重生成
  (9 任务)、目录树与 NET 小节同步、ftp_speed_test→regression/
  integrity、文档表补四条专项索引
- 工具:gen_task_table.py 补 iec104Task/protectTask 职责;
  gen_barrier_svg.py 补 iec104Task 消费者与 bit0 计数
2026-10-14 19:07:48 +08:00

31 KiB
Raw Blame History

STM32F4-Base 存储架构说明

1. 概述

本 项目在 GD5F2GQ5UE SPI NAND Flash(256MB)上实现了三层存储软件栈:

存储软件栈分层(应用 → FatFs → dhara FTL → 驱动 → SPI1)

上图由 tools/arch_diagram/gen_storage_stack_svg.py 生成(手绘 SVG,布局确定、层间单向箭头);改分层后编辑脚本顶部 L_* / ROWS 数据重跑即可。

物理分区布局(256MB 单分区)

上图由 tools/arch_diagram/gen_partition_svg.py 生成(分区宽 ∝ MB 数 + 偏移标尺);改分区后编辑脚本顶部 FIELDS 数据重跑即可。

数据路径:

  • 应用 → FatFS → dhara FTL → gd5f2gq5ue 驱动。FTL 提供 LBA 到物理页的映射、磨损均衡、坏块管理和垃圾回收,FatFS 通过标准 disk_* 接口访问 FTL 提供的块设备。

初始化顺序:

HAL_Init → SystemClock_Config → MX_GPIO/DMA/USART1/SPI2/SPI1/I2C1/UART4/IWDG/ADC1/TIM3_Init
 → app_main_init()                        // 应用层初始化(全程内核前)
   ├─ syslog_mcu_init()                   // 日志先进 RAM 暂存(内核前安全空操作)
   ├─ sd2506_init() → sys_clock_init()    // 时间子系统优先(墙钟基准)
   ├─ rs485_init()
   ├─ gd5f2gq5ue_init()                   // NAND 底层驱动(含 BBT 全片扫描 + ECC 使能)
   ├─ tpafe5160_init() → adc_input_init() // TPAFE + L1 采集层(语义映射 + ADC1 DMA)
   ├─ phasor_init() / freq_init() / prot_set_init() + prot_elm_init()  // L2/L3 内核前就绪
   ├─ SD2506 诊断(ID/时间/温度/电池)
   └─ HAL_TIM_Base_Start_IT(&htim3)       // 启动 TIM3 1kHz 采样节拍
 → osKernelInitialize() → MX_FREERTOS_Init() → osKernelStart()   // FreeRTOS 启动
   ├─ defaultTask → f_mount() → 卷健康探针/自愈 + 配置引擎 + syslog 落盘
   ├─ netTask     → 网络协议栈轮询(等 STORAGE_READY)
   ├─ ftpTask     → lftpd_start("/", 21)(等 NET_READY + STORAGE_READY)
   ├─ adcTask     → TIM3 1kHz 唤醒,ADC 采集(纯消费者)
   ├─ iec104Task  → IEC 60870-5-104 上行(:2404)
   ├─ protectTask → 保护判据 20ms 节拍
   └─ rs485Task   → 未创建(B2 暂缓,见 docs/任务规划/暂缓-RS485.md)

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:

#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 0) spare byte 0(列地址 0x800,`docs/芯片手册/GD5F2GQ5UExxG.md` §12.4 初始坏块标记位)
    └─ 非 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 块第一页(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(慎用,仅在重建时使用)
  • gd5f_bbt_scan() — 在 gd5f2gq5ue_init() 阶段、ECC 使能前扫描构建 BBT;运行时不再提供公开重扫接口(见 GD5F2GQ5UE_Trap_Records.md)
  • gd5f2gq5ue_print_bbt() — 打印 BBT 摘要(调试用)

注意:gd5f_private_bbt_scan() 选择在 ECC 使能前扫描。按docs/芯片手册/GD5F2GQ5UExxG.md §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 处开始吐数据

一页的内部布局(docs/芯片手册/GD5F2GQ5UExxG.md §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] 开始回数据——这正是出厂坏块标记位(docs/芯片手册/GD5F2GQ5UExxG.md §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 概述

FTL 是 NAND Flash 上方最重要的组件,功能包括:

功能 说明
地址映射 将 FatFS 的逻辑扇区号 (LBA) 映射到 NAND 物理页
磨损均衡 均匀分布擦除次数,延长芯片寿命
垃圾回收 回收无效页面空间 (GC)
坏块管理 擦除/编程失败时自动跳过并标记坏块
ECC 处理 检测和上报硬件 ECC 不可纠正错误

5.2 数据结构

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.4.1 resume 的 epoch 锚点 —— 回绕后必须锚"最新"而非"最高地址"(2026-10-02 修复)

⚠️ 这是"复位后所有写入全部丢失"的曾经真凶,排查存储丢失时第一顺位检查它。

背景:dhara 的 checkpoint 头页带 1 字节 epoch,journal 每绕满一圈 (roll_stats(),即 head 回绕到 0)epoch++。resume 时用 epoch 过滤,只认"当前圈"的 checkpoint。

坑:环回绕之后,"最高地址"的 checkpoint 属于上一圈(旧 epoch),而最新 checkpoint 落在低地址(块 0 一侧,epoch = 旧 + 1)。若把锚点取成 find_last_checkblock_any()(= 最高地址块),就会锚到旧 epoch,随后的 find_last_checkblock() 只能选中旧块 → find_head() 在旧块里找空页 → journal 整体回退到 回绕之前,回绕之后写的一切(日志、配置、FTP 上传)复位即丢。 (2026-09-20 的本地补丁正是这么改的,用于修"自愈卷重格式化每次开机丢失",属顾此失彼的回归。)

现行实现(Lib/dhara/journal.c,代码内附详细注释)—— 比较两个候选 epoch:

eFirst(最低地址块)与 eLast(最高地址块) 含义 锚点
相等 未回绕 相同,任取
eFirst == eLast + 1 (mod 256) 已回绕,低地址更新 eFirst
其它 低地址是上一圈残留 eLast

可观测性:NAND_FTL 开机加打一行 Map head=… tail=… epoch=…。 只看 Map resumed: N/M sectors used 判断不出回退(覆盖写不改 m->count, 实测该值在回退期间连续 2 小时一字不差);判据是 head 复位后是否倒退。 台架实测:回退时 head 96 → 130752、tail 115143 → 115068;修复后 head=96 / epoch=1。

5.5 页面缓存策略

FTL 之上还有一个 单页写回缓存 (write-back cache):

  • 读命中: 直接返回 s_cache_buf 数据
  • 读未命中: 刷出脏页 → 读新页到缓存
  • 写: 写入缓存 → 标记脏
  • 全页写入: 直接刷出 (跳过缓存)
  • 同步 (CTRL_SYNC): 刷出脏页 + dhara_map_sync

6. FatFS 集成

6.1 配置 (ffconf.h)

#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
gd5f_bbt_scan() init 时扫描构建 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 文件操作

#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 格式化

#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 中更新并立即通过 gd5f_bbt_save() 持久化到保留块池;下次复位后 gd5f_bbt_scan() 重建出厂坏块,gd5f_bbt_load() 再叠加保留池中保存的运行期坏块,因此运行期坏块在掉电后依然有效(详见 §4.8)。
  5. 功耗 — 擦除操作最大耗时约 5ms (驱动超时设为 5s),页编程约 600ms (超时 1s),读写操作快。在低功耗场景需注意合理安排操作时序。
  6. 缓存一致性 — 单页缓存 (s_cache_buf) 仅对 FatFS 层可见,多任务读写同一文件需在应用层同步。

10.1 台架实证增补(2026-09,CCM 扩容 + 存储套件排障沉淀)

  1. 单写者规则 — 存储由 defaultTask 独占:挂载 / f_mkfs / 卷健康探针 / net_cfg_load / syslog 落盘 / 配置引擎全收敛于它。netTask / uiTask 不得直接触碰配置引擎或 FatFS。启用存储测试套件(TEST_SUITE_STORAGE)时存储所有权整体移交 testTask,defaultTask 必须完全跳过存储 bring-up(App/task/default_task.c 中 #if !defined(TEST_SUITE_STORAGE) 包裹,2026-09-25 自 Src/freertos.c 迁出)——否则后台文件写与测试的 nand_ftl_deinit 掉电模拟并发,互相踩坏 FTL/FatFs 状态(实测:mkfs 半途冻结 + BBT 被误写)。
  2. 内存布局与 DMA/CCM 约束 — FreeRTOS 堆(含全部任务栈)已迁至 CCM 0x10000000(64KB,直连 M4 零等待),但 DMA1/DMA2 物理不可达 CCM。GD5F 驱动 GD5F_DMA_THRESHOLD = 1024:栈上的小缓冲(dhara meta 读 132B、dhara_nand_is_free 64B)一律走 CPU 轮询、绝不经 DMA;页级大块(2048)才走 DMA 且源/目的全部是文件作用域静态缓冲(落 SRAM)。新增任何 DMA 收发缓冲必须①文件作用域静态②大小≥阈值③确认落 SRAM。违反任一条 = 静默数据损坏(CCM 首轮扩容实验实证)。换料注意:克隆片可能缺 SRAM3(CCM),必须重测。
  3. LVGL 绘制缓冲越界护栏 — LVGL 9.3.0 对 I1(1bpp) 整帧渲染会在其算出的 stride*h(160×160/8=3200) 之后多写 1 个字节,紧邻 g_ui_draw_buf 之后的 .bss(如 ui_menu_view 的 s_frame_box)指针最低字节被打成 0xFF → 野指针 → HardFault(PC=lv_obj_get_scroll_y)。ui_port.c 的 g_ui_draw_buf 已预留 UI_DRAW_BUF_GUARD=32 字节护栏吸收该越界写(逻辑尺寸仍按 UI_FB_SIZE 上报,渲染数学不变)。勿删护栏、勿把缓冲缩到与邻接 .bss 紧邻。Lib/LVGL 为第三方不改。
  4. FatFs 严格 8.3 短名 — 本移植 Lib/FatFs/ffconf.h 为 FF_USE_LFN=0,只支持严格 8.3 短名:主名 ≤8 字符、扩展 ≤3 字符、不得以点开头(2026-09-20 台架实证,.fschk 无限重格式化事故根因)。.fschk 会被 ff.c 的 create_short 把「点之后」当扩展域解析而超限 → f_open 恒返 FR_INVALID_NAME(6);这是纯 RAM 里的名字判定,不碰盘。ff.c create_short 全部拒绝点的地方已加 DEBUG 打印:[ERR][FATFS] SFN reject (原因) seg='名字片段' bad_ch=坏字符——非法名字调用看开机日志即可定位。自愈类逻辑严禁把名字/参数类错误码(FR_INVALID_NAME 等)归入"卷损坏"触发集(整卷重格式化不可逆)。补充:含冒号 : 的名字被 FatFs 先解析为盘符前缀 → f_open 返回 FR_INVALID_DRIVE(11) 而非 FR_INVALID_NAME(6);两者都属合法拒绝(测试用例须同时接受)。
  5. FA_OPEN_APPEND 陷阱 / 短写 — FA_OPEN_APPEND(0x30) = FA_OPEN_ALWAYS|FA_SEEKEND,不含 FA_WRITE(0x02)——不带写位打开的文件 f_write 恒返回 FR_DENIED(7)(这是当年 /log 写失败的真根因);追加写必须写 FA_WRITE | FA_OPEN_APPEND(2026-09-20 台架实测,Lib/FatFs 移植版)。该移植的 f_write 在簇分配失败时返回 FR_OK + 短写(没有错误码)→ 判失败必须校验写回的字节数。标志位为旧式编号:FA_READ=0x01 / FA_WRITE=0x02 / FA_CREATE_NEW=0x04 / FA_CREATE_ALWAYS=0x08 / FA_OPEN_ALWAYS=0x10 / FA_OPEN_APPEND=0x30 / FA_SEEKEND=0x20(ff.c 内部定义),与上游 FatFs R0.14 不同,勿照抄上游代码示例。
  6. 崩溃可观测性(勿屏蔽) — 已开启 configCHECK_FOR_STACK_OVERFLOW=2 + configUSE_MALLOC_FAILED_HOOK + HardFault 现场报告(Src/stm32f4xx_it.c:裸 UART 打印 PC/LR/xPSR + CFSR/HFSR/BFAR/MMFAR 后复位,对照 .map 定位符号)。defaultTask 1024 words、testTask 2048 words 是实测下限,勿降。开机日志出现 [ERR] STACK OVERFLOW task= / [ERR] HARDFAULT ... / MALLOC FAILED 是精确定位线索,禁止屏蔽或降级。
  7. BBT 恢复模式 — 运行期坏块会随 TC-STO-03 注入累积;当介质被误标脏(如 DMA 误打 CCM 栈导致随机块被写坏并持久化)时,打开 test/board/storage_test.h 的 ENABLE_STORAGE_RECOVERY_TESTS 编译恢复构建,testTask 会执行 gd5f2gq5ue_bbt_rebuild()(出厂扫描清除运行期误标)+ nand_ftl_format() 清账后复位。恢复后容量回到基线(即扣除 BBT 保留池后的正常容量,非出厂裸容量)。
  8. lftpd 路径口径(归档更正 2026-10-13:原误记于 docs/问题记录/CH395F_Trap_Records.md——非 CH395F 议题,移入本节) — FTP STOR/CWD 曾频发 FR_INVALID_NAME(6):真约束即上条第 10 项的严格 8.3。原记录"FatFS 不接受 / 前缀 / 空字符串失败"两条与仓内 Lib/FatFs/ff.c 矛盾(ff.c:3101-3109 会剥前导分隔符、:3127-3129 空名返回根目录),已删。现行口径:lftpd_io.c 的 to_fatfs_path() 剥 / 前缀(行为正确,保留)+ 本仓路径用 "0:" 卷前缀(FF_FS_RPATH=0 下 "." 不是合法路径段)。长文件名需 FF_USE_LFN=1 + FF_LFN_UNICODE(显著增 RAM,未启用)。

10.2 FTP 服务(lftpd)鉴权语义(2026-10-11,N-19)

FTP 根目录 = 卷根,lftpd 是卷的网络文件入口(Drivers/BSP/NET/lftpd/)。N-19 起启用口令鉴权——[ftp] 配置段、鉴权流程与改口令方法的权威口径在 docs/子系统设计/网络配置说明.md §3.4,此处只登记存储侧语义:

  • 未登录命令门 — 登录成功前仅 USER/PASS/QUIT/FEAT/SYST/NOOP 放行,其余命令(含 RETR/STOR/LIST/DELE)一律 530。修复前 cmd_user/cmd_pass 对任意口令无条件回 230 = 匿名全读写整卷(N-19 根因)。
  • STOR/DELE 写门 — [ftp] write=0 时写命令一律 550 拒,卷对 FTP 侧只读(/log、/config.ini 仍可下载不可改写)。
  • 全局失败锁定 — 连续 5 次鉴权失败 → 锁定 60 s(跨会话全局、易失、重启清零);纯 lftpd 层状态,不触存储。
  • 默认口令告警锚点 — 出厂 pwd_hash 在用期间每次启动打 INFO [FTP] using factory default password。