4a4ad6bc5f
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名, 不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体 run_boot_test.py 的真实调用。 - AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改 「编译(/keil-build)」「串口回归(/serial-boot-test)」 - keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree 方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围) - 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入 等 41 行历史证据保留旧名 - gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑 - 顺手补并行方案同行漏网计数 51→52 钩子 验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52 钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
124 lines
9.5 KiB
Markdown
124 lines
9.5 KiB
Markdown
# 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`(B9 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 的恢复构建会把它链进去——所以这条坑只在
|
||
**做恢复的时候**才存在,也正因如此平时不会有迹象。
|