# STM32F4-Base 存储维护与容量恢复 > 版本:v1.0 日期:2026-09-21 > 适用工程:STM32F4-Base(STM32F407ZGTx + GD5F2GQ5UE SPI NAND + dhara FTL + FatFs) > 关联:`docs/测试说明/板级测试/GD5F2GQ5UE_Test_Guide.md`(破坏性用例清单)、`docs/内存管理/内存内容分配策略.md`(DMA 可达性守卫) ## 1. 三个维护动作,别混用 | 目标 | 开关组合 | 做了什么 | 代价 | |---|---|---|---| | **A. 卷/BBT 恢复** | `TEST_SUITE_STORAGE` + `ENABLE_STORAGE_RECOVERY_TESTS` | 重建 BBT(重扫出厂坏块并持久化)+ `nand_ftl_format()` 清 dhara map | 卷内容全失;**不恢复容量** | | **B. 容量恢复** | A 再叠加 `STORAGE_RECOVERY_FULL_WIPE` | 擦净 dhara 可见区 **2044 块**(连 journal checkpoint 一起)+ 重建 BBT;随后**断电重启** | 卷内容全失;2044 次块擦除(实测约 4 秒) | | **C. 日常回归** | 只开 `TEST_SUITE_*`,**不开** `ENABLE_DESTRUCTIVE_TESTS` | 只跑只读/参数校验用例,不写不擦不改坏块表 | 无 | 三个开关都在 `test/board/test_config.h`(A/B 的根开关在 `test/board/storage_test.h`),**默认全关**;维护完请重新注释并编译产品构建。 ## 2. 为什么"容量"需要单独恢复(根因) - 健康满容量 = **96064 pages(187 MB)**(`No valid map found, creating fresh` 分支下的满值)。 - dhara 的坏块计数 `bb_current` / `bb_last` **持久化写在 journal checkpoint 头里** (`Lib/dhara/journal.c` 的 `hdr_set_bb_current/hdr_set_bb_last`,随 checkpoint 一起落盘)。 容量按 `good_blocks = num_blocks − max(bb_last, bb_current) − 1` 递减。 - `dhara_journal_clear()`(即 `nand_ftl_format()` 走的路)只做 `bb_last = bb_current`,**不归零**—— 所以动作 A 清不掉被扣掉的容量,断电重扫也一样(计数从 checkpoint 读回)。 - 只有让下次启动 `dhara_map_resume` **失败**、走 `dhara_map_clear` 的**全新初始化**分支 (此时 `bb_last = bb_current = 0`)才能回满 → 这就是动作 B 的原理。 - 记账的触发:一次 `program verify` 失败(日志 `[ERR][NAND_FTL] program verify: data mismatch, blk N`) 就让 dhara 多记一个坏块;破坏性套件(写完擦、坏块注入、格式化)会累积这个数字。 ## 3. 操作步骤(以容量恢复为例) 1. `test/board/test_config.h`:取消注释 `TEST_SUITE_STORAGE`; 2. `test/board/storage_test.h`:取消注释 `ENABLE_STORAGE_RECOVERY_TESTS` 与 `STORAGE_RECOVERY_FULL_WIPE`; 3. `/keil-build`(要求 0 错误 0 警告); 4. 采集顺序 `open` → `clear` → 烧录 → `wait-idle`(擦除 2044 块约 4 秒,静默判据给 20~30s); 5. 确认日志: ``` === Storage Test: TC-STORAGE-04 maintenance (recovery / full wipe) === [PASS] TC-STORAGE-04 bbt rebuild wipe progress: ... 2044/2044 wiped 2044 blocks (N skipped as bad, 2044 visible) [PASS] TC-STORAGE-04 full wipe (2044 blocks erased) [PASS] TC-STORAGE-04 persisted BBT version=N === Storage Recovery: 3/3 PASSED (failed=0) === ``` 6. **断电重启**(`stlink_flash.py reset` 无参即可)——本次启动才会重建干净 journal; 7. 恢复产品构建(两个头文件重新注释)→ `/keil-build` → 烧录 → `/serial-boot-test`,确认: ``` [NAND_FTL] No valid map found, creating fresh (err=3) [NAND_FTL] NAND FTL ready (capacity: 96064 pages = 187 MB) [FS] no FAT volume, formatting NAND... ← 首次会重建 FAT [FS] FATFS mounted ``` ## 4. 注意事项 - **单写者**:套件运行期间存储由 testTask 独占(defaultTask 打印 `mount/cfg skipped (STORAGE suite owns storage)`),勿并发访问。 - **卷内容全失**:A/B 都会丢掉 `/log` 历史与 `/config.ini`(配置回落工厂默认,FAT 由启动流程自动重建)。 - **坏块跳过**:全片擦除会自动跳过 BBT 中已记录的坏块(`skipped as bad`),保留块池(BBT 存储区)不动。 - **别用 A 期待容量回来**:A 只重建 BBT + 格式化,容量仍是被扣掉的值——这是 2026-09-21 实测确认过的。 - **别为了"省一次擦除"而手工擦单块**:容量是否恢复取决于 checkpoint 是否全部失效,只擦单块无法保证(`resume` 会锚定到更早的 checkpoint)。 - 恢复后建议跑一次只读套件确认(GD5F 只读 16/16 或存储只读 3/3)。 ### 4.1 根目录残留文件清理(2026-10-22 实测,动作 C 之外的日常维护) 台架 `/` 根目录常年堆着 FTP 探针留下的文件——**不占坏块、不影响挂载,但会把"目录列表类" 问题的现场搞混**,也让容量读数失真。本次实测残留 **27 个文件 / 262 KB**(`AB000x`、`A000x`、 `R000x`、`SZ2K~64K`、`E12A`、`DIAG_A`、`PP0~PP4`,全是历史一次性探针留下的,那些探针脚本本身 已于同日删除,见 `MDK-ARM/README.md`)。 - **判据:只删显式点名的文件,绝不 glob**。清理脚本里写死 27 个文件名逐个 `DELE`;保留清单 必须含 **`/config.ini`**(网口与 FTP 鉴权配置)、**`/PROT_SET.BIN`**(保护定值 blob, `PROT_SET_BLOB_SZ` = 176 B,删了等于把定值清零)、**`/log`** 目录(运行与自检记录)。 - 三步验收:① 清完 `LIST` 只剩 `LOG` + `CONFIG.INI` + `PROT_SET.BIN`;② `/serial-boot-test` PASS(定值巡检 与配置加载都在这条链上);③ 跑 `test/host/ftp_list_fill_test.py` 确认 FTP 面正常。 - 这属于**动作 D(清文件)**,与本文 §1 的 A(重建 BBT + 格式化)/ B(全片擦除)/ C(容量恢复) 都不同:A/B 会丢卷内容,D 不动文件系统结构。 ### 4.2 自检台账清零,重建联合判定审计基线(2026-10-22 实测) `run_boot_test.py --require-selftest-pass --selftest-scope all`(累计口径)在台架上**永远 FAIL**: `/log/selftest.log` 里躺着 29 条历史记录,`2026-10-10 09:24:12 → 2026-10-14 06:55:02`, 组成为 **16 × `watchdog reset` + 13 × `heap low water`**——都是看门狗引入期与堆迁移期 留下的,两件事后来都已结案。新判据(只看本次新增)已不受影响,但**审计口径要能用,就得 有一块干净的基线**,所以按用户决定清掉台账。 - 安全性判据:`sys_log_mcu.c` 是**每条记录开-写-关**(`f_open(FA_WRITE|FA_OPEN_APPEND)` → `f_write(64)` → `f_close`),**不持有常驻句柄** → 从 FTP 侧 `DELE` 不会撕裂,下次有 故障时自动重建文件。清理前先把导出副本留档(1856 B = 29×64)。 - **产品数据一条不动**:`run.log`(运行记录)、`wdev.log`(掉电取证,带 8 KB 护栏)、 `soe.log`(SOE 记录 SOE 动作记录)——这三条是固件在写的活文件,容量护栏由固件自己管,别手删。 - **顺带清掉的孤儿**:`/log/flushchk.bin` **32,768 B**,全库零引用——已退役的 flush 诱导实验 留下的(`sys_log_mcu.c` 注释里那句"旧称 flush 诱导"就是它)。**删前先 RETR 出来验过内容**: 256 种字节值全部出现、前 64 字节全 `FF` → 就是一份 0..255 图案的写盘校验填充,没有任何 产品语义;留档后再 `DELE`(判据同 §4.1:先知道删的是什么)。 - 清理后 `/log` 只剩三条、**全是产品数据**:`run.log`(运行记录)、`wdev.log`(掉电取证)、 `soe.log`(SOE 动作记录)。之后再跑 `run_boot_test.py --require-selftest-pass` → **PASS** (699 ms、`SELFTEST_BASELINE: 既有记录 0 条`)。 - 验收(两条判定路径都要过):`--require-selftest-pass` → `SELFTEST_BASELINE: 既有记录 0 条 (status=absent)` → PASS;`--selftest-scope all` → `selftest: no fault record` → **PASS** (清零前这条是 `29 fault record(s)` FAIL)。设备侧无此文件时 lftpd 回 450,被按"台账干净" 处理,这条映射见 `../../架构设计/STM32F4-Base存储架构说明.md` §10.3 与 S7 判定表。 ### 4.3 恢复流程里的 ECC 开关:不能只看"跑完了"(2026-10-23) 动作 A/B 的第一步是 `gd5f2gq5ue_bbt_rebuild()`:临时**关掉 ECC** 才能读原始出厂坏块标记,扫完 必须**重新打开并验证真的生效**。这一"关—开"以前是两条**裸调用**(返回值全丢),于是存在一条 静默路径:**SPI 抖一下 → ECC 没能重新打开 → 设备以为恢复完成,之后所有 NAND 读写都在无位翻转 保护下运行**,而且 `gd5f_check_ecc()` 在 ECC 关闭时恒读"无 ECC 错误",整条链不会再有任何信号。 - 现在(2026-10-23 改)关 ECC 失败会**立即中止**(此刻 ECC 仍开着,是安全侧;带着 ECC 去扫 spare 反而会把 厂标好块误判成坏块,"重建 BBT"变成制造坏块),重开失败则复用 init 的做法**回读 B0h 校验 `ECC_EN` 位**,"写了但没生效"与写失败同罪,一律返回错误。 - **运维判据**:串口/`run.log` 里出现 `BBT rebuild: ECC restore FAILED` 或 `ECC not enabled (B0h=…)` → 本次恢复**不算完成**,设备处于无 ECC 保护态,必须**断电重启**让 init 重走一遍并使回读校验通过, 不要接着跑 `nand_ftl_format()` 当作已修好。反之 `BBT rebuilt and persisted` 才代表扫描 + 使能 + 持久化三件事都成了。 - 另记一条定位事实:`gd5f2gq5ue_bbt_rebuild()` 在产品镜像里**不被链接**(map 明写 `Removing … (604 bytes)`,产品构建无调用者),只有本动作 A/B 的恢复构建会把它链进去——所以这条坑只在 **做恢复的时候**才存在,也正因如此平时不会有迹象。