72b4b0943c
Replace task/batch shorthand IDs and residual layer labels in human-facing docs with semantic phrases. Add the new cleanup policy to docs/编号规范.md and update the entry docs. Machine keys (TC/CODE-RULE/MENU-STATE/TRAP/FR), hardware designators/pins/MAC, protocol/version numbers and frozen historical evidence are intentionally kept. Docs only; no code behavior change. Verified with check_doc_links.py --all and git diff --check.
658 lines
39 KiB
Markdown
658 lines
39 KiB
Markdown
# STM32F4-Base 存储架构说明
|
||
|
||
## 1. 概述
|
||
|
||
本 项目在 GD5F2GQ5UE SPI NAND Flash(256MB)上实现了三层存储软件栈:
|
||
|
||

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

|
||
|
||
> 上图由 `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 + 语义取数与有效性层(语义映射 + ADC1 DMA)
|
||
├─ phasor_init() / freq_init() / prot_set_init() + prot_elm_init() // 全周傅氏相量/保护判据内核前就绪
|
||
├─ 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 → 未创建(RS-485 暂缓,见 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`:
|
||
|
||
```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 数据结构
|
||
|
||
```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` | 检查页空闲 | **整页扫描 2048 B 判全 0xFF**(栈上 64 B 缓冲分块读;页首命中即早退,见下方 ⚠) |
|
||
| `dhara_nand_copy` | 页拷贝 (GC 用) | read + prog 组合 |
|
||
|
||
所有回调自动将 dhara 逻辑块/页加 `FTL_START_BLOCK` 偏移转换为物理地址。
|
||
|
||
⚠ **两条移植层硬约束(2026-10-15)**:
|
||
|
||
1. **`is_free` 必须整页精确**。dhara 的 journal 用它判断"检查点组是否未编程"(`journal.c:416 cp_free`、`journal.c:520` 找下一空闲页),其正确性前提是"答空闲的页必须真的全 0xFF"(`journal.c:405-414` 的注释论证)。旧实现只看页首 64 B → 一个"页首 0xFF、后段有数据"的已编程页会被误判空闲,dhara 遂**往已编程页再编程一次** = 位碰撞、journal/map 指向垃圾(数据丢失,且不报任何错)。本卷跑的是 FatFS 数据页,这种页型很常见(刚格式化的卷、稀疏文件块、日志尾部)。成本上做了快路径:页首 64 B 有数据就当场返回非空,与旧实现同价,只有"目前读到的全是 0xFF"的页才继续扫后段。
|
||
2. **六个 HAL 入口一律做几何上界校验**(上界 = `n->num_blocks << n->log2_ppb`)。越界页/块号(含 dhara 哨兵 `DHARA_PAGE_NONE` = 0xffffffff)旧实现直接加偏移送进 SPI 命令,会被芯片地址位**截断成某个真实页** → 静默读写/搬走无辜数据。现越界时 prog/read/copy/erase 返回 -1 且 `*err == DHARA_E_CORRUPT_MAP`,`is_free` 返回 0(保守判非空)、`is_bad` 返回 1(保守判坏)、`mark_bad` 直接返回(绝不标无辜好块)。**错误码选择是刻意的**:不能用 `DHARA_E_BAD_BLOCK`(`journal.c:904 recover_from` 会据此标坏块并跳块),也不能用 `DHARA_E_RECOVER`(`map.c:275 try_recover` 会据此启动回收);其余错误一律原样上抛,不会静默清 map(教训:宁报错不销毁数据)。
|
||
|
||
### 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
|
||
- **空闲 flush 兜底(2026-10-25,道F 第二轮)**: defaultTask 主循环按"存储就绪 +
|
||
卷空闲(`vol_lock_wait(0)` try)+ 周期节流(5 s)"周期性下发一次 `CTRL_SYNC`,
|
||
把"缓存留脏、之后再无调用触发 sync"的漏网脏页其掉电驻留窗封顶为一个周期;
|
||
卷锁被持有(syslog 落盘 / 配置保存 / lftpd open...close 进行中)时跳过,
|
||
**不改变文件持有期的既有语义**。clean 态 = 脏标志 + journal 双短路、零 NAND
|
||
触点;脏态成本与一次 `f_close` 的同步段同型。实现:`App/fs/vol_idle_sync.[ch]`
|
||
(单测 `test/host/vol_idle_sync/`)。
|
||
- ⚠ **边界(勿当成掉电保护)**: 本板**无掉电检测硬件**,"掉电前强制 flush"
|
||
不可行;单次写事务进行中掉电、以及 FTP STOR 文件持有期的落盘边界,均不因
|
||
空闲兜底改变。
|
||
|
||
---
|
||
|
||
## 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 |
|
||
| `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 文件操作
|
||
|
||
```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 中更新并立即通过 `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 扩容 + 存储套件排障沉淀)
|
||
|
||
7. **单写者规则** — 存储由 **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 被误写)。
|
||
8. **内存布局与 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),必须重测。
|
||
9. **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 为第三方不改。
|
||
10. **FatFs 严格 8.3 短名** — 本移植 `Lib/FatFs/ffconf.h` 为 `FF_USE_LFN=0`,**只支持严格 8.3 短名**:主名 ≤8 字符、扩展 ≤3 字符、**不得以点开头**(2026-09-20 台架实证,`.fschk` 无限重格式化事故根因)。
|
||
- **推论(2026-10-24 补齐,代码侧唯一入口 `s_is_selfheal_code()`)**:既然名字/参数类错误码在 8.3 配置下**必然重现**(同名同因),它们就**严禁**被归进任何"卷/文件坏了"的自愈触发集——否则自愈会退化成反复删自己的数据。本仓落点:`App/log/sys_log_mcu.c` 的触发集只收 `FR_DISK_ERR` / `FR_INT_ERR` / `FR_NO_FILESYSTEM` / `FR_DENIED`,`FR_INVALID_NAME` / `FR_NO_PATH` / `FR_NO_FILE` / `FR_INVALID_OBJECT` 一律立即失败(逐码依据 + `Lib/FatFs/ff.c` 行号写在该函数上方资料块;`App/log/soe_store.c` 侧同源取更保守子集并互指)。**本条拦下的真事**:旧集合含名字类码,后果是路径/卷异常期间**每轮排空删一次自己的历史日志且永远修不好**,由主机单测照出(TC-SYSLOG-101 / 102、TC-SOESTORE-022b),详见 `docs/测试说明/单元测试/主机单测覆盖率口径.md` §9「第 1 轮修复处置」。。`.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);两者都属合法拒绝(测试用例须同时接受)。
|
||
11. **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 不同,**勿照抄上游代码示例**。
|
||
12. **崩溃可观测性(勿屏蔽)** — 已开启 `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` 是精确定位线索,禁止屏蔽或降级。
|
||
13. **BBT 恢复模式** — 运行期坏块会随 TC-STORAGE-03 注入累积;当介质被**误标脏**(如 DMA 误打 CCM 栈导致随机块被写坏并持久化)时,打开 `test/board/storage_test.h` 的 `ENABLE_STORAGE_RECOVERY_TESTS` 编译**恢复构建**,testTask 会执行 `gd5f2gq5ue_bbt_rebuild()`(出厂扫描清除运行期误标)+ `nand_ftl_format()` 清账后复位。恢复后容量回到基线(即扣除 BBT 保留池后的正常容量,非出厂裸容量)。
|
||
14. **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 起启用)
|
||
|
||
FTP 根目录 = 卷根,lftpd 是卷的网络文件入口(`Drivers/BSP/NET/lftpd/`)。FTP 鉴权启用——`[ftp]` 配置段、鉴权流程与改口令方法的**权威口径**在 `docs/子系统设计/网络配置说明.md` §3.4,此处只登记存储侧语义:
|
||
|
||
- **未登录命令门** — 登录成功前仅 `USER`/`PASS`/`QUIT`/`FEAT`/`SYST`/`NOOP` 放行,其余命令(含 RETR/STOR/LIST/DELE)一律 `530`。修复前 `cmd_user`/`cmd_pass` 对任意口令无条件回 `230` = **匿名全读写整卷**(鉴权缺失的根因)。
|
||
- **STOR/DELE 写门** — `[ftp] write=0` 时写命令一律 `550` 拒,卷对 FTP 侧只读(`/log`、`/config.ini` 仍可下载不可改写)。
|
||
- **全局失败锁定** — 连续 5 次鉴权失败 → 锁定 60 s(跨会话全局、易失、重启清零);纯 lftpd 层状态,不触存储。
|
||
- **默认口令告警锚点** — 出厂 `pwd_hash` 在用期间每次启动打 INFO `[FTP] using factory default password`。
|
||
|
||
### 10.3 FTP 日志级别划分:对端行为 vs 固件故障(2026-10-22)
|
||
|
||
lftpd 原先把全部异常路径一律按 error 级打(串口上带 `[ERR]`),其中**九处其实由外部输入决
|
||
定**:客户端关窗口、连上就走、PASV 之后不建数据连接、运维改 IP 触发监听重建。代价实测两条:
|
||
|
||
1. `/serial-boot-test` 的 Forbidden 标记含 `[ERR]`(它同时是四个 Fault 崩溃日志的前缀)——"FileZilla 关了
|
||
个窗口"就能把一次启动测试判 FAIL。实测就是这样踩到的:抓自检台账基线那次 FTP 会话结束后,
|
||
设备打 `[ERR] [FTP] error reading next command`,本次 `/serial-boot-test` 直接 FAIL。
|
||
2. 任何对 21 端口"连一下就断开"的外部行为,都能**无限制造固件级 ERROR 日志**——日志面与门禁
|
||
面都可被外部输入驱动,这在工业现场是过不了审的。
|
||
|
||
| 级别 | 串口形态 | 覆盖的事件 | 判据 |
|
||
|---|---|---|---|
|
||
| error(`lftpd_log_error` / `DBG_ERROR`) | `[ERR] …` | 建 socket / bind / listen 失败、取端口失败、写通道失败、文件打开失败(读/写/重复打开)、`DOWNLOAD FAILED` / `UPLOAD FAILED`(传输未完成)、`data listener disarmed`(控制连接还活着而我方数据监听已不在服役) | 本侧缺陷,或**数据完整性事件** |
|
||
| peer(`lftpd_log_peer` → info 级) | 普通 INFO 行 | 控制通道被对端关闭(原 `error reading next command`)、发送 220 之前对端已走、`listener rebuilt`(网络重建,原因由 accept 层自己记)、四处 `data accept failed`(PASV 后没来数据连接)、`accept timeout` | **对端决定的正常收尾** |
|
||
|
||
- **新增专门的 `lftpd_log_peer()` 而不是就地改成 `DBG_INFO`**:让"谁才算真故障"在读代码时
|
||
一目了然,也防止后来者顺手把对端行为又升回 error。
|
||
- **"传输未完成"仍留 error**:对端中途退出确实让一次 STOR/RETR 没走完,那是需要留痕的完整性
|
||
事件(`../问题记录/CH395F_Trap_Records.md` 的中止家族就靠这些锚点定位)。
|
||
- **边界(实测划清)**:客户端**干净收尾**(发 `QUIT`,或 FIN 半关)现在整条链路上没有任何 `[ERR]`;
|
||
但用 **RST 硬拆**(`SO_LINGER` 清零、进程被杀、网线拔了)仍会留一条 `[ERR][NET] sockN TIMEOUT`——
|
||
那是 net 层的套接字超时锚点,网络自愈架构的判据要读它,**不在本次降级范围**。所以"采集窗口内
|
||
不要跑 FTP"这条规避口径仍然有效,只是不再因为一次正常下载而中弹。
|
||
- 排障信息量不减:peer 级照旧打印,且比原来更准(带 `ret=` / `sk=` / `timeout(ms)`,措辞从
|
||
"error reading next command"改成"control channel closed by peer"——原先那句把正常收尾
|
||
写成了故障,误导过排查方向。
|
||
|
||
### 10.4 dhara 失败类:ECC / 预算耗尽 / 完整性故障(2026-10-23)
|
||
|
||
一次 NAND 读写要穿过 **SPI → `gd5f_*` → dhara(`dhara_error_t`)→ FTL glue → FatFs `RES_ERROR`**
|
||
五段,而 glue 以前把 dhara 的所有失败**压成一个 `GD5F_ERROR`**——上层分不清"该重试"、
|
||
"该标坏块"、还是"块池见底了,重格式化也没用"。现在 `nand_ftl.c` 在**最早同时知道
|
||
LPN 与原始码的位置**就地分类并打印(`ftl_err_name()` 给可读名;**本仓 dhara 没有编
|
||
`error.c`,所以没有 `dhara_strerror()`,用它直接链接失败**——表是自己的一张短表):
|
||
|
||
| dhara 错误 | 归类 | 处置上为什么不同 |
|
||
|---|---|---|
|
||
| `ECC` / `TOO_BAD` | `BSP_ERR_CRC` | 位翻转不可纠或页太坏:**是页级问题**,读重试 / 由 dhara 换页,不代表卷坏 |
|
||
| `JOURNAL_FULL` / `MAP_FULL` | `BSP_ERR_NO_SPACE` | 介质还健康,是 **journal / 映射层块池见底**:该走容量恢复(`../测试说明/专项验证/存储维护与容量恢复.md`),此时重格式化只是把问题推回去 |
|
||
| `BAD_BLOCK` / `RECOVER` / `NOT_FOUND` / `CORRUPT_MAP` / 越界 | `BSP_ERR_IO` | 映射或介质**完整性**故障:保卷 + 上报,不许自愈重格式化 |
|
||
|
||
- 这条分类与真空白卷判据是同一条纪律的两端:那边规定"只有扫描全程 I/O 干净且无
|
||
checkpoint(`DHARA_E_TOO_BAD`)才算真空白卷",本表则保证失败**可读**——两者合起来
|
||
才是"别把瞬态毛刺当卷损坏去 `f_mkfs`"(旧行为:单次 I2C/SPI 抖动 → 清 map → 整卷
|
||
重格式化 → 静默销毁全部用户数据)。
|
||
- **仍未闭环**:分类目前**只进日志**;`disk_read`/`disk_write` 对 FatFs 仍只能回 `RES_ERROR`
|
||
(那是 FatFs 的契约)。要让 defaultTask 的卷健康探针**按类分支**,还需要一个跨调用
|
||
存活的失败类快照(`nand_ftl_last_error()`,8 B;代价是要先定多写者纪律——
|
||
defaultTask 与 testTask 都会碰存储)→ 即登记簿条目「可选全序列卷锁」的待还项。
|