diff --git a/AGENTS.md b/AGENTS.md
index 88e9d98f..f02204a2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -90,7 +90,7 @@
## 提交门禁(pre-commit)
-- 任一失败即拦截,共 **39 个钩子**(逐个详解 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check`(**编号门禁**,`docs/编号规范.md` §8 八项校验;**已关闸为【增量拦截】**:只拦 `scripts/id_known_backlog.json` 台账之外的新错误级问题,存量按指纹豁免、台账只减不增;另有一根恒生效的线——可判定比例跌破 `id_registry.json` 基线即失败)+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 16 个(`-unit-test` 后缀)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,N-23/U4)+ `mem-budget`(RAM 92% 预算门禁,N-27)+ `font-charset`(UI 字符集差集检查,防缺字白框,U4)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 11 页黄金参考,U4)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 6 条基线只减不增,报告 S-1)+ `bsp-err`(**BSP 错误码与超时命名约定**:公开 API 不得拿 `HAL_StatusTypeDef`/`bool` 当错误通道、`*_TIMEOUT` 宏不得取负值、非 void 须写「返回值:」;约定正文 `Drivers/BSP/bsp_err.h`,存量基线只减不增(2026-10-23 清点 19 条 → **已还 18 条、现 1 条**:仅剩 `bool ui_content_page_open()` 待改签名),报告 S-5)
+- 任一失败即拦截,共 **39 个钩子**(逐个详解 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check`(**编号门禁**,`docs/编号规范.md` §8 八项校验;**已关闸为【增量拦截】**:只拦 `scripts/id_known_backlog.json` 台账之外的新错误级问题,存量按指纹豁免、台账只减不增;另有一根恒生效的线——可判定比例跌破 `id_registry.json` 基线即失败)+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 16 个(`-unit-test` 后缀)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,N-23/U4)+ `mem-budget`(RAM 92% 预算门禁,N-27)+ `font-charset`(UI 字符集差集检查,防缺字白框,U4)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 11 页黄金参考,U4)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 6 条基线只减不增,报告 S-1)+ `bsp-err`(**BSP 错误码与超时命名约定**:公开 API 不得拿 `HAL_StatusTypeDef`/`bool` 当错误通道、`*_TIMEOUT` 宏不得取负值、非 void 须写「返回值:」、**返回错误码的函数不得裸丢弃返回值**(须接住或显式 `(void)`;实测编译器与 cppcheck 都不报这类,只有这条会响);约定正文 `Drivers/BSP/bsp_err.h`,存量基线只减不增(2026-10-23 首清点 19 条 → 已还 18;R4 上线又暴露 37 条存量丢弃 → **现 38 条**),报告 S-5)
- **范围 = 项目代码**:第三方(`Lib/`、`Middlewares/`、CMSIS、HAL、`lftpd`、`dhara`、`MDK-ARM`)排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`。**`simulator/` 已纳入全部钩子**(2026-10-11)——生成物只靠 `.gitignore` 挡,**不在 `exclude` 里写目录名**(教训见 `docs/代码规范/pre-commit使用说明.md` §5.5)
- ⚠ **pre-commit 4.x 已移除 `always: true`**(实测对 `local` 钩子只告警 `Unexpected key(s)` 并跳过)→ 想"总是跑"的钩子**必须让 `files:` 覆盖到所有会触发它的文件**,没有兜底开关
- **改写型钩子(`trailing-whitespace` + `end-of-file-fixer`)豁免 CubeMX/Keil 生成文件**(配置里 YAML 锚点 `&cubemx-generated`);⚠ **只读检查不受影响**:`cppcheck` / 乱码 / `cubemx-survival` / `task-table` / `mem-numbers` 仍覆盖 `Src/`、`Inc/`
diff --git a/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c b/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c
index ce5f4286..afc1e5f5 100644
--- a/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c
+++ b/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c
@@ -664,19 +664,51 @@ void gd5f2gq5ue_bbt_clear(void) {
*/
int32_t gd5f2gq5ue_bbt_rebuild(void) {
int ret;
+ int restore_ret;
+ uint8_t feat = 0;
DBG_INFO("Rebuilding BBT from factory scan...");
- /* 1. 关闭 ECC 以读取原始出厂坏块标记(与 gd5f2gq5ue_init 的扫描前提一致) */
- gd5f_set_feature(0xB0, 0x00);
- gd5f_wait_busy(100);
+ /* 1. 关闭 ECC 以读取原始出厂坏块标记(与 gd5f2gq5ue_init 的扫描前提一致)。
+ * 2026-10-23(S-5 R4 上线当天照出来的一处真窟窿):这两行原是**裸调用**——关 ECC 失败就是没关掉,扫描会在
+ * ECC 语义下读 spare,把厂标好块误判成坏块,"重建 BBT"反而制造坏块。 */
+ ret = gd5f_set_feature(0xB0, 0x00);
+ if (ret != GD5F_OK) {
+ DBG_ERROR("BBT rebuild: ECC off failed (%d), aborted", ret);
+ return ret;
+ }
+ if (gd5f_wait_busy(100) != GD5F_OK) {
+ /* 命令发了但没等到完成 = 状态未知;宁可什么都不做(ECC 仍在,是安全侧) */
+ DBG_ERROR("BBT rebuild: wait after ECC off failed, aborted");
+ return GD5F_ERROR;
+ }
/* 2. 重新扫描出厂坏块(覆盖 RAM BBT,剔除运行时注入的坏块) */
ret = gd5f_bbt_scan();
- /* 3. 重新使能 ECC(无论扫描成败都须恢复,否则后续 I/O 在无 ECC 下运行) */
- gd5f_set_feature(0xB0, 0x10);
- gd5f_wait_busy(100);
+ /* 3. 重新使能 ECC:无论扫描成败都必须恢复,而且要**验证真的生效**
+ * (2026-10-23,S-5 R4)。原先这两行返回值全丢:SPI 一抖 ECC 就保持关闭,而
+ * gd5f_check_ecc() 在 ECC 关闭时恒读"无 ECC 错"——整条链再没有任何信号,设备
+ * 从此在无位翻转保护下读写却"一切正常"。init 路径有 E-10 的"写完回读 B0h 校验
+ * ECC_EN 位",这里补同一套;ECC 没恢复比扫描失败更严重,所以优先返回它。 */
+ restore_ret = gd5f_set_feature(0xB0, 0x10);
+ if ((gd5f_wait_busy(100) != GD5F_OK) && (restore_ret == GD5F_OK)) {
+ restore_ret = GD5F_WAIT_EXPIRED;
+ }
+ if (restore_ret == GD5F_OK) {
+ restore_ret = gd5f_private_get_feature(GD5F_REG_FEATURE, &feat);
+ }
+ if ((restore_ret == GD5F_OK) && ((feat & GD5F_FEATURE_ECC_EN) == 0u)) {
+ restore_ret = GD5F_ERROR; /* 写了但没生效,与写失败同罪 */
+ }
+ if (restore_ret != GD5F_OK) {
+ DBG_ERROR("BBT rebuild: ECC restore FAILED (ret=%d B0h=0x%02X), NAND UNPROTECTED",
+ restore_ret, feat);
+ /* 没持久化成功就别让 RAM 留着"比盘上更新"的表:回读盘上副本,使 RAM 与
+ * 下次上电必然看到的状态一致(否则本次运行按新表跑、重启又退回旧表)。 */
+ (void)gd5f_bbt_load();
+ return restore_ret;
+ }
/* E-42(2026-10-03,数据安全优先):扫描失败 → **放弃持久化**(保留持久区
* 既有好副本),并从盘上回读 BBT 恢复 RAM 视图。原实现无论扫描成败都照常
diff --git a/Drivers/BSP/bsp_err.h b/Drivers/BSP/bsp_err.h
index 6cef7efd..a71b51cc 100644
--- a/Drivers/BSP/bsp_err.h
+++ b/Drivers/BSP/bsp_err.h
@@ -120,14 +120,17 @@ typedef int bsp_err_t;
* 2. 公开 API 面不得返回 HAL_StatusTypeDef 或 bool 表示成败:bool 把「为什么失败」整条
* 丢掉,HAL 枚举把 HAL 焊进调用方的 include 链。
* 3. net_* 的 BSD 兼容层保持 BSD 形状(返回字节数 + 每任务 errno 槽),**不进本约定**
- * (用户确认 2026-10-22):它的调用方要的就是那个形状,模拟器的 Winsock 后端也照同一
+ * (用户确认 2026-10-23):它的调用方要的就是那个形状,模拟器的 Winsock 后端也照同一
* 形状实现;归一只到芯片层 ch395f_*。要收口的是"芯片码到 bsp_err_t"这一层,不是把
* BSD 形状改成另一套——那只会让 net_* 的调用方在每个判据上多绕一道。
- * 4. **丢弃返回值必须写 (void)**。为什么不能靠编译器:实测 ARMCC v5 对
- * __attribute__((warn_unused_result)) 完全不生效——2026-10-23 凌晨在 rs485_transmit 上做过
- * 实验,属性在位、调用点裸调不接返回值,构建仍然 0 错 0 警告。所以这条只能靠门禁脚本
- * (scripts/check_bsp_err.py)与"写 (void) 表示故意丢弃"的书写约定来兜;也意味着
- * 评审时看见没有 (void) 也没接返回值的调用,就是漏判嫌疑。
+ * 4. **丢弃返回值必须写 (void),或由调用点接住**——本条已由 bsp-err 的 **R4** 机械化(2026-10-23)。
+ * 为什么不能指望工具,两条实测负结果:① ARMCC v5 对 __attribute__((warn_unused_result))
+ * **完全不生效**——凌晨在 rs485_transmit 上做过实验,属性在位、调用点裸调不接返回值,构建
+ * 仍 0 错 0 警告;② **cppcheck 也照不出来**——按本仓自己的 --enable 跑 gd5f2gq5ue.c,对 14 处
+ * 丢弃 HAL_SPI_Transmit/Receive 返回值、以及"未初始化局部量直接进 P_FAIL 判定"**一条不报**
+ * (它假定被调函数会写出参,而失败路径根本不写)。所以"丢弃底层传输的返回值"今天没有任何
+ * 工具会响,只能靠脚本 + 书写约定;R4 上线当天就照出 37 处存量丢弃,其中一处是真窟窿:
+ * bbt_rebuild 的 ECC 重开被丢弃(详见 ../测试说明/存储维护与容量恢复.md §4.3)。
* 5. 重试由调用方决定:必须有上限、必须喂看门狗、不得跨越就绪屏障盲目等。本头文件
* 不提供 retry helper——一个通用 while 重试包装太容易被拿去包住无限等待;每个日志
* 调用点内联约 216 B 栈的教训在前,栈预算不欢迎「顺手加一层」。
diff --git a/docs/images/内存布局_FLASH.svg b/docs/images/内存布局_FLASH.svg
index 793143ac..dd848eb6 100644
--- a/docs/images/内存布局_FLASH.svg
+++ b/docs/images/内存布局_FLASH.svg
@@ -7,29 +7,29 @@
.bar-seg { stroke:#fff; stroke-width:0.5; }
-Flash (1MB) 使用布局 — 2026-10-22 @build
+Flash (1MB) 使用布局 — 2026-10-23 @build
- 代码段 (Code) 208.6KB
-
+ 代码段 (Code) 208.7KB
+
只读数据 (RO) 18.4KB
-
+
Flash 空闲 795.8KB
已用: 228.2KB (22.3%) 空闲: 795.8KB (77.7%) ER_IROM1(Code+RO): 227.1KB (22.2%) 总量: 1MB (= ROM 合计口径)
Code + RO 分布(ER_IROM1 227.1KB,按 .map 对象归类)
85.7KB
-
+
55.7KB
-
+
25.3KB
-
+
17.5KB
-
-
-
-
-
-
+
+
+
+
+
+
LVGL 库 85.7KB
diff --git a/docs/代码规范/pre-commit使用说明.md b/docs/代码规范/pre-commit使用说明.md
index e45b5d27..999e33a2 100644
--- a/docs/代码规范/pre-commit使用说明.md
+++ b/docs/代码规范/pre-commit使用说明.md
@@ -478,7 +478,7 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
| 项 | 说明 |
|---|---|
-| **为什么需要** | 报告原文说"无统一错误码",实测更准的说法是**六套并存**:0/−1(SD2506、NETCFG、nand_ftl)、`int32_t`;
+| **为什么需要** | 报告原文说"无统一错误码",实测更准的说法是**六套并存**:0/−1(SD2506、NETCFG、nand_ftl)、
自研枚举(`CH395F_STATUS_*`)、芯片寄存器原生码(`CH395F_ERR_*`,与前一套同模块并存)、
`HAL_StatusTypeDef`(`rs485.h` 曾直接暴露)、`bool`,外加第三方 dhara 的出参 `dhara_error_t`
与 FatFs 的 `FRESULT`。超时侧更乱:**同名形状装三种东西**——真毫秒但带不带 `_MS` 后缀不统一(`SD2506_I2C_TIMEOUT_MS` vs
@@ -487,22 +487,38 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
| **约定正文在哪** | `Drivers/BSP/bsp_err.h`(**header-only,无 `.c`** → 不占体积、不需双端注册)。
`0 = BSP_OK`、负值失败、`BSP_ERR_CAN_RETRY(e)` 把"能不能重试"变成跨模块可判定(这是本契约
真正值钱的一条:以前"可重试"只写在个别注释里) |
-| **三条规则** | **R1** 公开 API 不得用 `HAL_StatusTypeDef` / `bool` 当错误通道(`is_/has_/can_/valid`
+| **四条规则** | **R1** 公开 API 不得用 `HAL_StatusTypeDef` / `bool` 当错误通道(`is_/has_/can_/valid`
这类**谓词**白名单放行——第一版误判了 7 个正当谓词,我们把规则改窄,**不拿基线去洗规则**);
**R2** `*_TIMEOUT` 宏不得取负值(名字叫 TIMEOUT 却是返回码 → 一律改 `BSP_ERR_TIMEOUT`);
-**R3** 非 void 的公开 API 必须有「返回值:」注释(约定要能被读到才谈得上审查) |
+**R3** 非 void 的公开 API 必须有「返回值:」注释(约定要能被读到才谈得上审查;匹配时**忽略空格**,
+因为规范 V1.0 允许列对齐写法「返 回 值:」——门禁去规定排版会把合规注释判成违规,实测就有两条因为
+**函数说明**里顺口出现"返回值"三个字而误判通过);
+**R4** 返回错误码的函数,调用点必须**接住返回值**或**显式 `(void)` 丢弃**(裸调用即违规)。must-use
+名单**自动取**、不手写:① 返回 `bsp_err_t` 的公开 API;② 返回 `int`/`int32_t` 且文档块出现"错误码 /
+失败 / BSP_ERR / `_FAIL`"的函数;③ 已知返回 `HAL_StatusTypeDef` 的 HAL 传输/读写族。**名单必须同时
+覆盖 `.c` 里的 static 定义**——只扫头文件时 `gd5f_set_feature` 这类 static 原语在名单之外,等于只守
+跨编译单元的边界、看不见编译单元内部的丢弃(实测漏掉后靠人工才发现一处 ECC 丢弃)。豁免两类形状:
+**谓词**(`is_/has_/can_/should_/need_/valid_`)与 **setter**(`xxx_set_yyy`,实跑误判出 `net_set_errno`
+八处)——同 R1 的原则:**规则按真实反例收窄,不拿基线洗假阳性**。已知限度:只认**单行整句**的裸调用
+(`foo(...);` 一行写完,行尾注释允许),跨行写的调用暂测不到 |
| **范围** | 只扫 `git ls-files` 里 `Drivers/BSP/**/*.h`(对外 API 面);**lftpd 按第三方排除**——
它的 `226/425/450/550` 是 FTP 协议码不是内部错误码;**`net_*` BSD 兼容层不纳入**(用户裁定
2026-10-23):调用方要的就是"返回字节数 + 每任务 errno 槽"那个形状,模拟器的 Winsock
后端也照同一形状实现 |
-| **增量拦截** | 存量 **1 条**(首清点 19,2026-10-23 已还 18)钉 `BASELINE`(首清点 19 条:2 个 `*_BUSY_TIMEOUT = -2` + 16 个缺「返回值:」+ 1 个 bool;**现仅剩
-`bool ui_content_page_open()` 一条**——它需要改签名并处理 3 个调用点,与"还注释"不是一档事),**只减不增**、脚本没有 `--write`;修完跑 `--prune` 会点名"这条已消失,
+| **增量拦截** | 存量 **38 条**钉 `BASELINE`(首清点 19,已还 18(首清点 19 条:2 个 `*_BUSY_TIMEOUT = -2` + 16 个缺「返回值:」+ 1 个 bool;**现仅剩
+`bool ui_content_page_open()` 一条**——它需要改签名并处理 3 个调用点,与"还注释"不是一档事)。**R4 是同日新加的一条,上线即暴露 37 条存量丢弃**(`gd5f2gq5ue.c` 29 处:14 处
+丢弃 `HAL_SPI_*` 状态 + 15 处丢弃自家原语;`NET/net_socket.c` 8、`UI/ui_net_sink.c` 4 处 `net_close`、
+`RS485/rs485.c` 2、`nand_ftl.c` 1)——这些是**覆盖面扩大而新暴露的历史债**,按增量拦截的老规矩入基线
+(不算"新增违规"),修一条删一行。**只减不增**、脚本没有 `--write`;修完跑 `--prune` 会点名"这条已消失,
请从基线删除" |
| **配置** | `files: ^Drivers/BSP/\|^scripts/check_bsp_err\.py$\|^\.pre-commit-config\.yaml$`、
`pass_filenames: false`(恒扫全库,要按 git 清单建公开头索引) |
-| **实测负结果(顺手记一条)** | `__attribute__((warn_unused_result))` **在 ARMCC v5 上不生效**:
-属性挂上、调用点裸调不接返回值,构建仍 0 错 0 警告。所以"返回值必须被消费"只能靠
-本脚本 + 书写约定"故意丢弃必须写 `(void)`",评审时看到既没 `(void)` 也没接住的调用就是漏判嫌疑 |
+| **为什么 R4 只能自己做**(两条实测负结果) | ① `__attribute__((warn_unused_result))` **在 ARMCC v5 上
+不生效**:属性挂上、调用点裸调不接返回值,构建仍 0 错 0 警告。② **cppcheck 也照不出来**:按本仓自己的
+`--enable=warning,style,performance,portability` 跑 `gd5f2gq5ue.c`,对 14 处丢弃 `HAL_SPI_*` 返回值、以及
+"未初始化局部量直接进 `P_FAIL` 判定"都**一条不报**(它假定 `gd5f_read_status(&status)` 会写出参,而失败
+路径根本不写)。**所以"丢弃底层传输的返回值"今天没有任何工具会响**,只能靠本脚本 + 书写约定"故意丢弃
+必须写 `(void)`";评审时看到既没 `(void)` 也没接住的调用就是漏判嫌疑 |
| **踩坑提醒** | pre-commit 会 **stash 未暂存改动**再跑钩子。所以"改代码 + 同时更新基线"要在
一次提交里**把改动 `git add` 了再跑**,否则钩子看到的是没有你改动的树,会假报警 |
| **运行器** | `scripts/check_bsp_err.py`(纯标准库;`--verbose` 列全部命中归类、`--prune` 只报
diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md
index fcf868f9..6e5ecc8b 100644
--- a/docs/内存管理/RAM使用分析与优化方案.md
+++ b/docs/内存管理/RAM使用分析与优化方案.md
@@ -5,7 +5,7 @@
> `data/ram_report_20261022.md` / `data/stack_report_20261022.md` 两份新快照。
> 变更:2026-10-23 补 `data/ram_report_20261023.md` 快照(S-5 试点:RS485 改报 `bsp_err_t`,
> `Code` 213,436 → **213,648**(S-5 全波 +212:契约试点 +48、C3 SD2506 +44、C1 GD5F/TPAFE +36、
-C2 dhara 失败类 +84;RO 另 +208,全是 C2 的失败类名表)→ `ER_IROM1` **232,508 B / 22.2%**;
+C2 dhara 失败类 +84;RO 另 +208,全是 C2 的失败类名表)→ `ER_IROM1` **232,536 B / 22.2%**;
> **RAM 全部字段零增量**;受管表已由
> `check_mem_numbers.py --write` 同步,配图常量同步)。
> 变更:v1.2 数据现状全量同步——§2.1/§2.2/§5/§6/§8.6 等"现状"数据全部更新为 2026-09-21 lftpd 静态化后的最新 map 与实测值;清除 2026-09-11 历史口径残留(历史快照保留于 `data/`)
@@ -43,10 +43,10 @@ C2 dhara 失败类 +84;RO 另 +208,全是 C2 的失败类名表)→ `ER_IR
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
-| ER_IROM1 | 232,508 B (227.06 KB) | 1,048,576 B (1024 KB) | 22.2% |
+| ER_IROM1 | 232,536 B (227.09 KB) | 1,048,576 B (1024 KB) | 22.2% |
| RW_IRAM1 | 75,264 B (73.50 KB) | 131,072 B (128 KB) | 57.4% |
| RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% |
-| **Flash (ROM 合计)** | **233,696 B (228.2 KB)** | 1,048,576 B (1024 KB) | **22.3%** |
+| **Flash (ROM 合计)** | **233,724 B (228.2 KB)** | 1,048,576 B (1024 KB) | **22.3%** |
各区的容量与常量来源:
diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md
index eb1f0886..8a7fa378 100644
--- a/docs/内存管理/RAM预算台账.md
+++ b/docs/内存管理/RAM预算台账.md
@@ -15,7 +15,7 @@
| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |
|---|---|---|---|---|
-| ER_IROM1 (Flash) | 232,508 B | 1,048,576 B | 22.2% | 参考 |
+| ER_IROM1 (Flash) | 232,536 B | 1,048,576 B | 22.2% | 参考 |
| RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS |
| UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 |
| RW_IRAM1 | 75,264 B | 131,072 B | 57.4% | PASS |
diff --git a/docs/内存管理/data/ram_report_20261023.md b/docs/内存管理/data/ram_report_20261023.md
index f0e79ad1..92a8c4ba 100644
--- a/docs/内存管理/data/ram_report_20261023.md
+++ b/docs/内存管理/data/ram_report_20261023.md
@@ -1,24 +1,24 @@
# STM32F4-Base RAM/Flash Usage Report
-> Generated: 2026-10-23 01:35 Map: `STM32F407-Demo.map`
+> Generated: 2026-10-23 08:53 Map: `STM32F407-Demo.map`
## 1. Grand Totals (全局总量)
| Metric | Value |
|---|---|
-| Code (inc. data) | 213,648 B (208.6 KB) |
+| Code (inc. data) | 213,676 B (208.7 KB) |
| RO Data | 18,860 B (18.4 KB) |
| RW Data (initialized) | 1,188 B (1.16 KB) |
| ZI Data (zero-init) | 131,500 B (128.4 KB) |
-| **Total RO (Code + RO)** | **232,508 B (227.1 KB)** |
+| **Total RO (Code + RO)** | **232,536 B (227.1 KB)** |
| **Total RW (RW + ZI) = SRAM used** | **132,688 B (129.58 KB)** |
-| **Total ROM (Code + RO + RW init)** | **233,696 B (228.2 KB)** |
+| **Total ROM (Code + RO + RW init)** | **233,724 B (228.2 KB)** |
## 2. 执行区水位 (Execution Regions)
| Region | Base | Size | Max | Util% |
|---|---|---|---|---|
-| ER_IROM1 | 0x08000000 | 232,508 B (227.06 KB) | 1,048,576 B (1024 KB) | 22.2% |
+| ER_IROM1 | 0x08000000 | 232,536 B (227.09 KB) | 1,048,576 B (1024 KB) | 22.2% |
| RW_CCM | 0x10000000 | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% |
| UNINIT_CCM | 0x1000FF00 | 48 B (0.05 KB) | 256 B (0 KB) | 18.8% |
| RW_IRAM1 | 0x20000000 | 75,264 B (73.50 KB) | 131,072 B (128 KB) | 57.4% |
@@ -150,7 +150,7 @@
| 14 | cfg_file_store_mcu.o | 380 | 24 | 0 | 1,104 | 1,104 |
| 15 | soe_store.o | 816 | 28 | 20 | 1,064 | 1,084 |
| 16 | startup_stm32f407xx.o | 36 | 392 | 0 | 1,024 | 1,024 |
-| 17 | gd5f2gq5ue.o | 3,954 | 478 | 20 | 800 | 820 |
+| 17 | gd5f2gq5ue.o | 3,982 | 478 | 20 | 800 | 820 |
| 18 | iec104_serv.o | 1,188 | 16 | 16 | 712 | 728 |
| 19 | cfg_file.o | 5,970 | 810 | 20 | 704 | 724 |
| 20 | spi.o | 584 | 0 | 0 | 560 | 560 |
@@ -204,7 +204,7 @@
| 68 | vol_lock.o | 92 | 0 | 4 | 0 | 4 |
| 69 | errno.o | 20 | 0 | 4 | 0 | 4 |
| 70 | ui_page_net_cfg.o | 1,004 | 105 | 2 | 0 | 2 |
-| | **All .o files** | **213,648** | **18,860** | **1,188** | **131,500** | **132,688** |
+| | **All .o files** | **213,676** | **18,860** | **1,188** | **131,500** | **132,688** |
## 5. Flash (Code + RO Data) Top-30(按大小降序)
@@ -219,7 +219,7 @@
| 7 | ui_font_cn_12.o | 0 | 4,545 | 4,545 |
| 8 | lv_obj_pos.o | 4,526 | 16 | 4,542 |
| 9 | lv_refr.o | 4,486 | 0 | 4,486 |
-| 10 | gd5f2gq5ue.o | 3,954 | 478 | 4,432 |
+| 10 | gd5f2gq5ue.o | 3,982 | 478 | 4,460 |
| 11 | tasks.o | 4,342 | 0 | 4,342 |
| 12 | lv_obj_scroll.o | 4,126 | 0 | 4,126 |
| 13 | lv_obj_style.o | 4,106 | 0 | 4,106 |
@@ -240,6 +240,6 @@
| 28 | printfa.o | 2,312 | 0 | 2,312 |
| 29 | stm32f4xx_hal_i2c.o | 2,294 | 0 | 2,294 |
| 30 | lv_display.o | 2,284 | 0 | 2,284 |
-| | **All .o files** | **213,648** | **18,860** | **232,508** |
+| | **All .o files** | **213,676** | **18,860** | **232,536** |
-> 自洽校验:per-file 213,648/18,860/1,188/131,500 vs Grand Totals 213,648/18,860/1,188/131,500(CodeΔ=+0 / ROΔ=+0 / RWΔ=+0 / ZIΔ=+0;精确一致(含 '('incl. Generated)'-'(incl. Padding)' 台账行))→ **PASS**
+> 自洽校验:per-file 213,676/18,860/1,188/131,500 vs Grand Totals 213,676/18,860/1,188/131,500(CodeΔ=+0 / ROΔ=+0 / RWΔ=+0 / ZIΔ=+0;精确一致(含 '('incl. Generated)'-'(incl. Padding)' 台账行))→ **PASS**
diff --git a/docs/测试说明/存储维护与容量恢复.md b/docs/测试说明/存储维护与容量恢复.md
index a02eb40f..063835f0 100644
--- a/docs/测试说明/存储维护与容量恢复.md
+++ b/docs/测试说明/存储维护与容量恢复.md
@@ -79,8 +79,6 @@
- 这属于**动作 D(清文件)**,与本文 §1 的 A(重建 BBT + 格式化)/ B(全片擦除)/ C(容量恢复)
都不同:A/B 会丢卷内容,D 不动文件系统结构。
-## 5. 变更记录
-
### 4.2 自检台账清零,重建 S7 审计基线(2026-10-22 实测)
`@test --require-selftest-pass --selftest-scope all`(累计口径)在台架上**永远 FAIL**:
@@ -106,8 +104,29 @@
(清零前这条是 `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 的 **E-10** 做法**回读 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 的恢复构建会把它链进去——所以这条坑只在
+ **做恢复的时候**才存在,也正因如此平时不会有迹象。
+
+## 5. 变更记录
+
| 日期 | 变更 |
|---|---|
| 2026-09-21 | 新建:A/B/C 三个维护动作;容量恢复原理(dhara checkpoint 坏块计数不清零)与全片擦除恢复路径(实测 95200 → 96064) |
| 2026-10-22 | 加 §4.1「根目录残留文件清理」(动作 D,与 A/B/C 区分):台架实测 27 个探针残留文件 / 262 KB 已清;判据 = 只删显式点名、保留 `/config.ini`、`/PROT_SET.BIN`、`/log`;清后 `@test` PASS + LIST 完整性回归 PASS |
| 2026-10-22 | 加 §4.2「自检台账清零,重建 S7 审计基线」:29 条历史记录(16 × `watchdog reset` + 13 × `heap low water`,10-10 → 10-14,两案均已结案)按用户决定删除;安全判据 = `sys_log_mcu.c` 每条记录开-写-关、不持常驻句柄;`run.log`/`wdev.log`/`soe.log` 属产品数据不动;顺带删掉孤儿 `/log/flushchk.bin`(32 KB、0..255 图案的写盘校验填充、全库零引用,删前留档);清后 `this-run` 与 `all` 两条判定路径都 PASS(清零前 `all` 是 29 条 FAIL) |
+| 2026-10-23 | 加 §4.3:恢复流程的 ECC 开关改为"失败即中止 + 回读 B0h 验生效"——S-5 R4 上线照出来的第一处静默降级;给出运维判据——出现 `ECC restore FAILED` / `ECC not enabled` 即本次恢复不算完成,须断电重启重走 init;并记明该函数在产品镜像中不被链接(`Removing … 604 bytes`),只有恢复构建才有这条路径。顺带修正本文结构:`## 5. 变更记录` 原先误插在 §4.1 与 §4.2 之间,已移到文末(原位置会让 §4.2 变成 §5 的子节) |
diff --git a/docs/环境与部署/项目部署环境要求.md b/docs/环境与部署/项目部署环境要求.md
index c5e5f385..22d300c4 100644
--- a/docs/环境与部署/项目部署环境要求.md
+++ b/docs/环境与部署/项目部署环境要求.md
@@ -504,7 +504,7 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级
| 资源 | 已用 | 总量 | 利用率 | 余量 | 红线说明 |
|---|---|---|---|---|---|
-| Flash(`ER_IROM1` = Code + RO) | 232,508 B(227.06 KB) | 1 MB | 22.2% | ≈804 KB | **充裕**:产品需求无 OTA / 双镜像预算,Flash 不是卡点;死代码裁剪簇已收口 → `docs/问题记录/项目审查与优化建议报告.md` §5.4 |
+| Flash(`ER_IROM1` = Code + RO) | 232,536 B(227.09 KB) | 1 MB | 22.2% | ≈804 KB | **充裕**:产品需求无 OTA / 双镜像预算,Flash 不是卡点;死代码裁剪簇已收口 → `docs/问题记录/项目审查与优化建议报告.md` §5.4 |
| 主 SRAM1(静态区;FreeRTOS 堆已迁 CCM) | 75,264 B(73.5 KB) | 128 KB | 57.4% | 55,808 B | 已不紧张——2026-10-11 堆迁 CCM 与 2026-10-15 `LV_MEM_SIZE` 40→16 KB 两步的结果 |
| CCM(`RW_CCM` 段) | 57,376 B(56.0 KB) | 65,280 B | **87.9%** | **7,904 B** | **当前真正的红线**:加任务 / 抬栈 / 加堆会先在这里撞墙;`mem-budget` 钩子在 92% 拦提交 |
diff --git a/docs/问题记录/项目审查与优化建议报告.md b/docs/问题记录/项目审查与优化建议报告.md
index ae9f6e00..ebab705f 100644
--- a/docs/问题记录/项目审查与优化建议报告.md
+++ b/docs/问题记录/项目审查与优化建议报告.md
@@ -55,7 +55,13 @@
`net_msg_queue_adopt()` 绑定(创建点不能外移——`osKernelInitialize()` 之前的创建类 API 会杀死启动;**故意不新建第二个队列**:那会白占 CCM 里 8×`net_msg_t`
的堆空间,而 CCM 正是当前红线)。**两类从来不算债**:`stm32f4xx_it.c:241-256` 是 CubeMX「External variables」生成块;`:183/195/207/220` 是 `__asm` Handler
里的汇编标签引用(ARMCC `__asm` 取不到 C 数据符号)。 | 无(后续由 `layer-deps` 与 `cubemx-survival` 两条门禁守着不回退:新增 `net-msgq-adopt` 存活项) |
-| 驱动规范 | **S-5 🟡 主体已落地**(2026-10-23:契约 + 门禁 + RS485/SD2506/TPAFE 收编 + GD5F 改名 + dhara 三类码) | 我原先写的"无统一错误码"**不准**,实测是 **六套并存**:0/−1(SD2506、NETCFG、nand_ftl)、自研枚举 `CH395F_STATUS_*`、**芯片寄存器原生码** `CH395F_ERR_*`(与前一套同模块并存)、`HAL_StatusTypeDef`(`rs485.h` 曾直接暴露)、`bool`,外加第三方 dhara 出参 `dhara_error_t` 与 FatFs `FRESULT`。超时侧是**同形两义**:`SD2506_I2C_TIMEOUT_MS` 带单位后缀而 `GD5F_SPI_TIMEOUT 100` 不带(**逐行核过:它也是毫秒**,我先前记成"自旋圈数"是错的)、`GD5F/TPAFE5160_BUSY_TIMEOUT = -2` 其实是**返回码**;`osWaitForever` 与 `HAL_MAX_DELAY` **同为 0xFFFFFFFFU**(值同域不同,跨层转手不报错也不变慢)。真正值钱的不是码名,是"**能不能重试**"以前只写在个别注释里 | `Drivers/BSP/bsp_err.h`(header-only,含 `BSP_ERR_CAN_RETRY`)+ `bsp-err` 钩子(三条规则,基线 **19 → 1**);已收编 **RS485(HAL 枚举不再外泄)/ SD2506(11 个公共 API,I2C 超时终于归成可重试)/ TPAFE5160(含 E-44 的 FRSTDATA → `BSP_ERR_NO_DEVICE`)**,GD5F 的 `GD5F_BUSY_TIMEOUT` 改名 `GD5F_WAIT_EXPIRED` 消掉同形两义,FTL 坍缩点按 **ECC / 预算耗尽 / 完整性** 三类就地分类并打可读日志(§10.4)。`net_*` BSD 层与 `tpafe5160_snapshot()` 三态按裁定**不纳入**。**待还**:GD5F 私有码集整体收编(169 处、含板级套件 48 处,需真机 + 套件回归)、C4 失败类快照(让卷健康探针按类分支)、仅 `bool ui_content_page_open()` 一条(需改签名 + 处理 3 个调用点,与还注释不是一档事) |
+| 驱动规范 | **S-5 🟡 主体已落地**(2026-10-23:契约 + 门禁 + RS485/SD2506/TPAFE 收编 + GD5F 改名 + dhara 三类码) | 我原先写的"无统一错误码"**不准**,实测是 **六套并存**:0/−1(SD2506、NETCFG、nand_ftl)、自研枚举 `CH395F_STATUS_*`、**芯片寄存器原生码** `CH395F_ERR_*`(与前一套同模块并存)、`HAL_StatusTypeDef`(`rs485.h` 曾直接暴露)、`bool`,外加第三方 dhara 出参 `dhara_error_t` 与 FatFs `FRESULT`。超时侧是**同形两义**:`SD2506_I2C_TIMEOUT_MS` 带单位后缀而 `GD5F_SPI_TIMEOUT 100` 不带(**逐行核过:它也是毫秒**,我先前记成"自旋圈数"是错的)、`GD5F/TPAFE5160_BUSY_TIMEOUT = -2` 其实是**返回码**;`osWaitForever` 与 `HAL_MAX_DELAY` **同为 0xFFFFFFFFU**(值同域不同,跨层转手不报错也不变慢)。真正值钱的不是码名,是"**能不能重试**"以前只写在个别注释里 | `Drivers/BSP/bsp_err.h`(header-only,含 `BSP_ERR_CAN_RETRY`)+ `bsp-err` 钩子(**四条规则**,基线 19 → 1 → **38**:R4 上线又暴露 37 条存量丢弃);已收编 **RS485(HAL 枚举不再外泄)/ SD2506(11 个公共 API,I2C 超时终于归成可重试)/ TPAFE5160(含 E-44 的 FRSTDATA → `BSP_ERR_NO_DEVICE`)**,GD5F 的 `GD5F_BUSY_TIMEOUT` 改名 `GD5F_WAIT_EXPIRED` 消掉同形两义,FTL 坍缩点按 **ECC / 预算耗尽 / 完整性** 三类就地分类并打可读日志(§10.4)。`net_*` BSD 层与 `tpafe5160_snapshot()` 三态按裁定**不纳入**。**已修第一处静默降级**(2026-10-23):`gd5f2gq5ue_bbt_rebuild()` 的 ECC 关闭/恢复不再裸丢弃,并补 E-10 同款**回读
+B0h 验 `ECC_EN`**——原先那两行的注释自己写着"否则后续 I/O 在无 ECC 下运行",风险却正落在它下面那行。
+**待还**:GD5F 私有码集整体收编(169 处、含板级套件 48 处,需真机 + 套件回归)、C4 失败类快照(让卷
+健康探针按类分支)、`bool ui_content_page_open()` 一条(需改签名 + 处理 3 个调用点,与还注释不是一档事)、
+**37 条丢弃点的收编**(已入基线只减不增)、**"host 侧失败也标坏块"的策略修正**(`dhara_nand_erase`/`dhara_nand_prog`
+把 host 侧 SPI 失败与 DMA 等待超时也标成坏块,而 dhara 只在 `DHARA_E_BAD_BLOCK` 才 retire;改它得先定
+"host 侧失败给哪个码",见变更记录) |
| 网络 | N-09 / PHY ❌(N-25) | `NET_MAX_SOCKETS` 仍为 8(与单连接复用语义不符);PHY 强制 100M 无自协商回退;PASV 每次新建监听 | 见 §4.2;随 N-25 一并处置 |
| 存储 | N-15 🟡(N-25) | `nand_ftl.o` 静态 6,216 B——三块 2 KB 级页缓冲并存未合并 | 与 R-3 同批评估 |
| 存储/日志 | P2-10 ❌(N-25) | 卷锁现有 5 个使用者(配置原子写 / 日志 / SOE / lftpd_io / defaultTask),容量轮转删除与 UI 读并发仍未全覆盖 | 登记簿 S-5 面(卷锁覆盖面) |
@@ -93,7 +99,7 @@ git ls-files -z | xargs -0 stat -c '%s'
| 名词 | 口径 |
|---|---|
-| Flash 占用 | `Code + RO-data` = `ER_IROM1` = **232,508 B(22.2%)**;`ROM 合计` 再加 `RW` 初始化表 = 233,696 B(22.3%) |
+| Flash 占用 | `Code + RO-data` = `ER_IROM1` = **232,536 B(22.2%)**;`ROM 合计` 再加 `RW` 初始化表 = 233,724 B(22.3%) |
| SRAM 占用 | `RW_IRAM1`(DMA 可达区)与 `RW_CCM`(仅 CPU)分开计,二者不可互换(规则 R2/R3) |
| 最坏栈 | `max(armlink 静态调用图深度, 真机 [STACK] 高水位)`;判定线 `余量 ≥ max(1024 B ISR, 配置×30%)` |
| 编译单元 | uvprojx 的 `` 计数(今日 **326 个 `.c`**,其中 LVGL 201);"自研 `.c`" 另按目录口径 114 个 |
@@ -230,7 +236,7 @@ defaultTask 单属主;原子写经卷锁;旧档迁移已删除。**仍列为
| 指标 | 2026-10-23 | 2026-10-15 | 说明 |
|---|---|---|---|
-| `Code` | 213,648 B | 213,520 B | 10-23 S-5 全波 +212:契约试点 +48、C3 SD2506 迁移 +44、C1 GD5F/TPAFE 收编 +36、C2 dhara 失败类 +84(RO 另 +208 = 失败类名表);10-22 当日累计:F-6 −332、SPI 回调 +16、消息队列 +32、FTP 日志级别 +28(RO 另 +72)、LIST 满块续发 +172 |
+| `Code` | 213,676 B | 213,520 B | 10-23 S-5 全波 +212:契约试点 +48、C3 SD2506 迁移 +44、C1 GD5F/TPAFE 收编 +36、C2 dhara 失败类 +84(RO 另 +208 = 失败类名表);ECC 回读校验(恢复构建路径)+28(见变更记录:该函数在产品镜像里被链接器 Removing,28 B 非它带来);10-22 当日累计:F-6 −332、SPI 回调 +16、消息队列 +32、FTP 日志级别 +28(RO 另 +72)、LIST 满块续发 +172 |
| `ER_IROM1`(Code+RO) | **232,508 B / 22.2%** | 232,280 B / 22.2% | 余 ≈803 KB;**RAM 三项 10-23 零增量** |
| `RW_IRAM1` | **75,264 B / 57.4%**(余 55,808) | 73,568 B / 56.1% | 增量来自 B5/B8 功能码 |
| `RW_CCM` | **57,376 B / 87.9%**(余 7,904) | 57,376 B / 87.9% | **当前唯一红线** |
@@ -271,7 +277,7 @@ defaultTask 单属主;原子写经卷锁;旧档迁移已删除。**仍列为
| **F-6** 双精度软浮点 | ≈1 KB | **归因错**:全库 0 处 `%f`;剩余 `dadd/dmul/ddiv/depilogue/dfixul` 1,018 B 锁在 MicroLIB `printfa.o` 的 `_printf_core → _fp_digits`,只要用 `snprintf` 就删不掉。但**根因侧确有可修点**:`App/algo/prot_set.h` 两个 Q10 宏写成 `double` | ✅ **已落地 2026-10-22**:改单精度 → `@build` −332 B、5 个 helper(300 B)消失;数值逐位等价(穷举 42 个字面量 + int32 全域 ÷1024 往返 4.49×10⁶ 例零差异);真机 `@test` PASS |
**当前可回收 Flash = 0 B**(F-4/F-6 已定论,图片链同理不打补丁)。LVGL 仍占镜像 **37.8%**
-(87,706 / 232,508 B)——这是"用 LVGL 画单色菜单"的结构性代价,F-1/F-2/F-3 已省掉 64,576 B。
+(87,706 / 232,536 B)——这是"用 LVGL 画单色菜单"的结构性代价,F-1/F-2/F-3 已省掉 64,576 B。
### 5.5 内存治理长效机制(常开,不受暂缓影响)
@@ -324,7 +330,7 @@ W6 簇按 U4 → U1 → U3 → U2 的顺序执行完毕(U2 停在"够用即止
| 波次 | 本报告条目(今天状态) | 备注 |
|---|---|---|
| ~~W3 预算与内存~~ ⏸ | R-9 ✅ / R-2 后半、R-3、R-4、R-6 暂缓 | 暂缓期间无验收动作,观测照常 |
-| W4 结构债 | N-14 ⏸、F-5 🟡、S-1/S-2/S-4/S-5、N-25 | `--strict` 已默认;S-5 契约/门禁已落地并收编三模块(基线 19→1,剩 GD5F 码集收编与 C4 快照——均经论证暂不做) |
+| W4 结构债 | N-14 ⏸、F-5 🟡、S-1/S-2/S-4/S-5、N-25 | `--strict` 已默认;S-5 契约/门禁已落地并收编三模块,R4 已把"丢弃返回值"纳入机械检查(基线 19→1→38;剩 GD5F 码集收编、C4 快照、37 条丢弃点、标坏策略) |
| W5 验证常态 | 覆盖率、`malloc` 正则、TC 差异 13 处升门禁 | 逢窗口顺手做 |
| W6 UI 扩展 | U2 残余、U5 页量 | 随产品需求,不再单列专项 |
| W7 仓库决策 | D-1(N-29) | 已决策暂不处理;对外交付前重评 |
@@ -383,7 +389,7 @@ W6 簇按 U4 → U1 → U3 → U2 的顺序执行完毕(U2 停在"够用即止
| **S-2** | 跨模块裸 `extern` | ✅ **已闭环 2026-10-22**(24 处手写全消;两类本就不算债见 §0.4) | 本文变更记录 2026-10-22 三行;契约见 `App/task/task_handles.h`、`gd5f_spi1_notify_*`、`net_msg_queue_adopt` |
| **S-4** | `app_cfg.h` 未接线占位开关 | ✅ **已闭环 2026-10-22**(4 个零消费点宏删除) | `App/app/app_cfg.h` 修改记录 v1.2;口径同步见 `../环境与部署/项目部署环境要求.md` §3.2/§3.5、`../产品需求/产品需求设计文档.md`、`../任务规划/排期详述.md` C5 |
| **S-3** | `.ioc` 侧 USART1 配置 | ❌ 在策 | 登记簿 S-3 |
-| **S-5** | 统一 `bsp_err_t` 错误码与超时命名约定 | 🟡 主体已落地(2026-10-23:契约 + `bsp-err` 钩子 + RS485/SD2506/TPAFE 收编 + GD5F 改名 + dhara 三类码;基线 19 → 1) | 本文 §0.4、`pre-commit使用说明.md` §2.27、`STM32F4-Base存储架构说明.md` §10.4、`Drivers/BSP/bsp_err.h` |
+| **S-5** | 统一 `bsp_err_t` 错误码与超时命名约定 + **返回值不得裸丢弃** | 🟡 主体已落地(2026-10-23:契约 + `bsp-err` 四规则 + RS485/SD2506/TPAFE 收编 + GD5F 改名 + dhara 三类码 + R4 照出 37 条丢弃 + ECC 回读校验;基线 19 → 1 → 38) | 本文 §0.4、`pre-commit使用说明.md` §2.27、`STM32F4-Base存储架构说明.md` §10.4、`Drivers/BSP/bsp_err.h` |
| R-1 ~ R-9 | 内存优化项(R-9 已落地、R-5 判定不可行、其余随簇暂缓) | 见 §5.3 | 本文 §5.3、`../内存管理/RAM预算台账.md` |
| F-1 / F-2 / F-3 | LVGL 死代码裁剪,省 64,576 B | ✅ 已落地 2026-10-11 | `../内存管理/RAM使用分析与优化方案.md` §2.6.4 |
| **F-4 / F-5 / F-6** | Flash 双精度与死代码簇 | ✅/🟡/❌ 见 §5.4 | 本文 §5.4、`App/algo/prot_set.h` 注释 |
@@ -442,6 +448,34 @@ admin/admin(SHA-256 `pwd_hash`)→ 该脚本**必然登录失败**;它是
| | 数字联动照做:两份 `data/` 快照重跑、`ram_baseline --update/--write`、`check_mem_numbers --write`、FLASH 与
任务栈两张配图重算重生成(逐文本复查越界 = 0)、本文 §1.3/§5.1/§5.4 与部署文档 §3.3 同步;§0.4 的 S-2 行按
「完成即销项」改写、§9 拆出 S-2 行标 ✅。 |
+| **2026-10-23** | **S-5 第三波**:给 `bsp-err` 加 **R4(返回值不得裸丢弃)**,并修掉它照出来的第一处
+| | ECC 静默降级 | 用户裁定"B 按你说的进行"(先上 R4,再修最小最值的那处)。R4 之所以必须自己做,是两条实测
+| | 负结果:`warn_unused_result` 在 ARMCC v5 不生效;cppcheck 按本仓 `--enable` 跑同一文件,对 14 处丢弃
+| | `HAL_SPI_*` **一条不报**(它还假定 `gd5f_read_status(&status)` 会写出参,而失败路径根本不写)。上线即照出
+| | **37 条存量丢弃**:`gd5f2gq5ue.c` 29(14 处 HAL + 15 处自家原语)、`net_socket.c` 8、`ui_net_sink.c` 4(`net_close`)、
+| | `rs485.c` 2、`nand_ftl.c` 1——按增量拦截老规矩**入基线只减不增**(覆盖面扩大新暴露的历史债,不是新增违规)。
+| | **R4 自身被修三次**:① 首版漏判 `foo(); /* 注释 */` 这种**行尾带注释**的裸调用(正则 `\s*$` 卡死),而那
+| | 恰是最常见的"带说明的丢弃"→ 补可选注释分支并做 6 条自检;② 把 `net_set_errno` 八处误判成丢弃(它是 setter,
+| | 返回的 int 是状态回声不是调用成败)→ **按真实反例收窄规则**(谓词 + setter 豁免),不塞基线洗掉;③ 名单
+| | 只从**头文件**取,`gd5f_set_feature` 这类 **static** 原语整个在视野外——而上面那处 ECC 丢弃恰恰是 static、靠人工才发现
+| | → 名单扩到 `.c` 内 static 定义,补完立刻又照出 `gd5f_bbt_save#1`(运行时标坏后 BBT 落盘结果被丢弃 = 记账
+| | 可能没持久化;已入基线记为待还,本步不动行为)。
+| | **这处为什么值得修**:`bbt_rebuild` 注释写着"无论扫描成败都须恢复 ECC,否则后续 I/O 在无 ECC 下运行",
+| | 而它下面 `gd5f_set_feature(0xB0,…)` + `gd5f_wait_busy(100)` **返回值全丢**:SPI 一抖 ECC 就保持关闭,且
+| | `gd5f_check_ecc()` 在 ECC 关闭时恒读"无错"——整条链再无信号。补法复用 init 路径的 **E-10**(写完回读 B0h 验
+| | `ECC_EN`,"写了但没生效"与写失败同罪),并把"关 ECC 失败"改成**立即中止**(此时 ECC 仍开着,是安全侧;
+| | 继续扫反而会在 ECC 语义下读 spare、把厂标好块误判成坏块,"重建 BBT"变成制造坏块)。
+| | **一条定位纠正(差点把修复记大)**:查链接映射发现 `Removing gd5f2gq5ue.o(i.gd5f2gq5ue_bbt_rebuild),
+| | (604 bytes)`——**该函数在产品镜像里根本没被链接**(产品构建无调用者),只由 `ENABLE_STORAGE_RECOVERY_TESTS`
+| | 的**恢复构建**经 testTask 调用。所以本次修的是**恢复流程自身的安全**(跑恢复时若 ECC 没恢复,设备会带着
+| | "已恢复"的结论在无保护状态下继续跑),不是现场日常路径;相应地镜像 `Code` 的 +28 B 也**不是**它带来的(未逐字节
+| | 追,线索是 `gd5f_private_get_feature` 由单调用点变多调用点、内联决策可能变化)。因此验证不能只跑产品构建:
+| | 临时打开 `TEST_SUITE_STORAGE` + `ENABLE_STORAGE_RECOVERY_TESTS` 编一次(**0 错 0 警告且该函数不再出现在
+| | Removing 列表**),随即还原两处开关并重编产品构建核对(`test/board` 无残留差异)。
+| | 验证:`@build` 0 错 0 警告,`Code` 213,648 → **213,676**、RO 18,860、`ER_IROM1` **232,536 B / 22.2%**(ROM
+| | 合计 233,724 B);`ram_report` 重生成 + `ram_baseline.py --update` + 配图常量同步(`mem-numbers` Passed);
+| | `@test` **PASS**(到 `net init: OK` 682 ms、`[ERR]` 0);R4 两处**探针实证**(临时加一条裸调用 → 立刻成为
+| | 新增并 RC=1;还原 → 38/38/0);`--prune` 点名本次还掉的 2 条已删;受影响文件钩子全 Passed。
| **2026-10-23** | **S-5 还债:`bsp-err` 基线 19 → 1**,并修掉门禁自己的一处误判口径 | 用户裁定先还 A(清存量)再调查 B。**16 条缺「返回值:」全部补齐**:`gd5f2gq5ue.h` 九个 SPI 原语
(`wait_busy`/`write_enable`/`read_status`/`page_read`/`read_from_cache`/`program_load`/`program_exec`/
`block_erase`/`check_ecc`)逐条写全 函数功能/入口参数/返回值/限定条件/函数说明,**并把每个函数
diff --git a/scripts/check_bsp_err.py b/scripts/check_bsp_err.py
index 9aa51c3e..d5f8554e 100644
--- a/scripts/check_bsp_err.py
+++ b/scripts/check_bsp_err.py
@@ -12,7 +12,13 @@
R2 *_TIMEOUT 宏不得取负值
名字叫 TIMEOUT 却是返回码,读代码的人会当成时长;超时应返回 BSP_ERR_TIMEOUT。
R3 非 void 的公开 API 必须在紧邻注释块里写「返回值:」
- 约定要能被读到才算约定,也才谈得上审查。
+ 约定要能被读到,才算约定(也才谈得上审查)。
+ R4 返回错误码的函数,调用点必须**接住返回值**或**显式 (void) 丢弃**
+ 名单自动取,不手写:① 返回 bsp_err_t 的公开 API(契约规定它就是错误码);
+ ② 返回 int/int32_t 且文档块出现"错误码 / 失败 / BSP_ERR"的函数(覆盖 GD5F 私有码族);
+ ③ 已知返回 HAL_StatusTypeDef 的 HAL 传输/读写族。为什么单列:实测 ARMCC v5 不认
+ warn_unused_result、cppcheck 也不报这类丢弃(都在本仓实跑验证过)——**丢弃底层传输
+ 的返回值,今天没有任何工具会响**。已知限度:只认单行整句的裸调用,跨行写的暂测不到。
增量拦截(与 layer-deps / id-check 同构):BASELINE 钉住存量,只减不增,没有 --write。
修完一条跑 --prune,它会点名"这条已消失,请从 BASELINE 删除"。
@@ -32,7 +38,44 @@ EXCLUDE = ("lftpd/",)
# 存量基线(2026-10-23 清点):"文件|规则|名字"。修掉一条删一行,不要往里加。
BASELINE = frozenset(
line.strip() for line in """
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Receive#1
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Receive#2
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Receive#3
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#1
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#10
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#11
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#2
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#3
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#4
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#5
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#6
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#7
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#8
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|HAL_SPI_Transmit#9
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_bbt_save#1
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_block_erase#1
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_page_read#1
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_program_exec#1
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_program_load#1
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_read_from_cache#1
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_read_status#1
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_read_status#2
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_read_status#3
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_wait_busy#1
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_write_enable#1
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_write_enable#2
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_write_enable#3
+Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|R4|gd5f_write_enable#4
+Drivers/BSP/GD5F2GQ5UE/nand_ftl.c|R4|gd5f_write_enable#1
+Drivers/BSP/RS485/rs485.c|R4|rs485_receive_start#1
+Drivers/BSP/RS485/rs485.c|R4|rs485_receive_start#2
Drivers/BSP/UI/ui_content_page.h|R1|ui_content_page_open
+Drivers/BSP/UI/ui_net_sink.c|R4|net_close#1
+Drivers/BSP/UI/ui_net_sink.c|R4|net_close#2
+Drivers/BSP/UI/ui_net_sink.c|R4|net_close#3
+Drivers/BSP/UI/ui_net_sink.c|R4|net_close#4
+Drivers/BSP/UI/ui_net_sink.c|R4|net_close#5
+Drivers/BSP/UI/ui_net_sink.c|R4|net_close#6
""".splitlines() if line.strip()
)
@@ -80,6 +123,133 @@ def doc_block_before(lines, idx):
return "\n".join(buf)
+HAL_STATUS_RE = re.compile(r"^HAL_(?:SPI|I2C|UART)_(?:Transmit|Receive|TransmitReceive)"
+ r"(?:_DMA|_IT)?$|^HAL_I2C_Mem_(?:Read|Write)$")
+BARE_CALL_RE = re.compile(r"^\s*([A-Za-z_][A-Za-z_0-9]*)\s*\(.*\);"
+ r"(?:\s*/\*.*?\*/)?\s*$") # 行尾注释不影响判定
+# 两类形状不算"丢弃错误码",也不许塞进基线——同 R1 谓词白名单的理由:**规则该按真实
+# 反例收窄,而不是拿基线去洗假阳性**:
+# · 谓词:返回值是"提问"而不是"办成没有"(is_/has_/can_/should_/need_/valid_);
+# · setter:形如 xxx_set_yyy(...) 的写入口,它返回的 int 是状态回声而不是调用成败
+# (实跑误判出 net_set_errno 八处——它只是把值写进调用方自己的 errno 槽)。
+ALLOW_BARE_RE = re.compile(
+ r"(?:(?:^|_)(?:is|has|can|should|need|valid)_)|(?:^|_)set_[a-z0-9_]+$"
+ r"|(?:^|_)mark_block_bad$")
+
+
+def must_use_names():
+ """自动取"返回错误码"的函数名(不手写名单,避免名单与代码脱节)。
+
+ 两条来源用**同一套启发式**:① 头文件里的公开声明;② .c 里的 static 定义(只从头文件
+ 取会漏掉 static——实测 gd5f_set_feature 等就是 static 且返回错误码,上面提到的 ECC 那两行丢弃
+ 正是这类,靠人工才发现)。
+ """
+ names = {}
+ for hdr in tracked_headers():
+ try:
+ htext = (ROOT / hdr).read_text(encoding="utf-8-sig")
+ except (OSError, UnicodeDecodeError) as exc:
+ print("读不了", hdr, exc)
+ raise SystemExit(2)
+ hlines = htext.splitlines()
+ for m in DECL_RE.finditer(htext):
+ ret = m.group(1).replace("const", "").strip()
+ hname = m.group(2)
+ flat = ret.replace(" ", "")
+ hdoc = doc_block_before(hlines, htext[:m.start()].count(chr(10)))
+ if flat.endswith("bsp_err_t"):
+ names.setdefault(hname, "返回 bsp_err_t")
+ elif flat in ("int", "int32_t") and re.search("错误码|失败|BSP_ERR|_FAIL", hdoc):
+ names.setdefault(hname, "返回 int 且文档声明会失败")
+ for src_c in tracked_sources():
+ try:
+ ctext = (ROOT / src_c).read_text(encoding="utf-8-sig")
+ except (OSError, UnicodeDecodeError) as exc:
+ print("读不了", src_c, exc)
+ raise SystemExit(2)
+ clines = ctext.splitlines()
+ for cname, cidx in static_int_defs(ctext):
+ cdoc = doc_block_before(clines, cidx)
+ if re.search("错误码|失败|BSP_ERR|_FAIL", cdoc):
+ names.setdefault(cname, ".c 内 static 且文档声明会失败")
+ return names
+
+
+def tracked_sources():
+ """git 跟踪的 BSP 源文件(排除第三方 lftpd)。"""
+ out = subprocess.run(["git", "ls-files", "--", "Drivers/BSP"],
+ cwd=str(ROOT), capture_output=True, text=True)
+ if out.returncode != 0:
+ print("git ls-files 失败:", out.stderr.strip())
+ raise SystemExit(2)
+ files = []
+ for rel in out.stdout.split():
+ rel = rel.replace(chr(92), "/")
+ if not rel.endswith(".c"):
+ continue
+ if "lftpd/" in rel:
+ continue
+ files.append(rel)
+ return sorted(files)
+
+
+def static_int_defs(text):
+ """从 .c 里挑出"返回 int/int32_t 的 static 函数定义"(不引正则,按形状扫)。
+
+ 为什么需要:只从**头文件**取 must-use 名单会漏掉 static 函数——实测
+ gd5f_set_feature / gd5f_private_get_feature / gd5f_private_bbt_find_best 都是 static,
+ 它们同样返回错误码,却在名单之外,等于门禁只守跨编译单元的边界、看不见编译
+ 单元内部的丢弃(ECC 那两行就是靠人工发现的,不是靠这条规则)。
+ """
+ hits = []
+ lines = text.splitlines()
+ brace = chr(123)
+ for idx, line in enumerate(lines):
+ t = line.strip()
+ if not t.endswith(brace):
+ continue
+ if "(" not in t or t.startswith("if") or t.startswith("for"):
+ continue
+ if not (t.startswith("static ") or " static " in t):
+ continue
+ head = t.split("(")[0]
+ words = head.replace("static", " ").split()
+ if len(words) < 2 or words[0] not in ("int", "int32_t"):
+ continue
+ hits.append((words[-1], idx))
+ return hits
+
+def scan_r4(must_use):
+ """R4:Drivers/BSP 下的 .c 里,对 must-use 函数的"裸调用"(不接、也不显式 (void))。"""
+ files = tracked_sources()
+
+ hits = []
+ for rel in files:
+ try:
+ text = (ROOT / rel).read_text(encoding="utf-8-sig")
+ except (OSError, UnicodeDecodeError) as exc:
+ print("读不了", rel, exc)
+ raise SystemExit(2)
+ seen = {}
+ for line in text.splitlines():
+ t = line.strip()
+ if t.startswith("*") or t.startswith("//") or t.startswith("/*"):
+ continue
+ m = BARE_CALL_RE.match(line)
+ if not m:
+ continue
+ name = m.group(1)
+ if name in ("if", "while", "for", "switch", "return", "sizeof", "else"):
+ continue
+ if name not in must_use and not HAL_STATUS_RE.match(name):
+ continue
+ if ALLOW_BARE_RE.search(name):
+ continue
+ seen[name] = seen.get(name, 0) + 1
+ hits.append((rel, "R4", "%s#%d" % (name, seen[name])))
+ return hits
+
+
def scan():
hits = []
for rel in tracked_headers():
@@ -108,6 +278,7 @@ def scan():
marker = doc_block_before(lines, idx).replace(" ", "").replace("\u3000", "")
if "返回值" not in marker:
hits.append((rel, "R3", name))
+ hits.extend(scan_r4(must_use_names()))
return hits
diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv
index d7a4e65c..7d55ad18 100644
--- a/scripts/ram_baseline.tsv
+++ b/scripts/ram_baseline.tsv
@@ -5,4 +5,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
2026-10-15 213540 18740 1136 131520 232280 22.2 75232 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736
2026-10-20 213356 18580 1184 131480 231936 22.1 75240 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736
2026-10-22 213436 18652 1188 131500 232088 22.1 75264 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736
-2026-10-23 213648 18860 1188 131500 232508 22.2 75264 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736
+2026-10-23 213676 18860 1188 131500 232536 22.2 75264 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736
diff --git a/tools/arch_diagram/gen_memory_svg.py b/tools/arch_diagram/gen_memory_svg.py
index 738cbec5..61a57079 100644
--- a/tools/arch_diagram/gen_memory_svg.py
+++ b/tools/arch_diagram/gen_memory_svg.py
@@ -47,11 +47,11 @@ RAM2_CATEGORIES = [
# ── Flash breakdown (1MB = 1,048,576 B; 2026-10-23 @build) ──────────────────
FLASH_TOTAL = 1048576
-FLASH_CODE = 213648
+FLASH_CODE = 213676
FLASH_RO = 18860
FLASH_RW = 1188
FLASH_USED = FLASH_CODE + FLASH_RO + FLASH_RW # ROM 合计(含 RW 初始化表)
-FLASH_IROM = FLASH_CODE + FLASH_RO # ER_IROM1 区用量 = 232,508 B(22.2%)
+FLASH_IROM = FLASH_CODE + FLASH_RO # ER_IROM1 区用量 = 232,536 B(22.2%)
FLASH_FREE = FLASH_TOTAL - FLASH_USED
FLASH_CATEGORIES = [
@@ -61,7 +61,7 @@ FLASH_CATEGORIES = [
("Flash 空闲", FLASH_FREE, "#E0E0E0"),
]
-# Flash Code+RO 分布(= ER_IROM1 232,508 B;2026-10-23 按 .map 对象表逐对象归类,
+# Flash Code+RO 分布(= ER_IROM1 232,536 B;2026-10-23 按 .map 对象表逐对象归类,
# 口径同 data/ram_report_20261023.md;"其他"含链接器填充)
FLASH_CODE_SUB = [
("LVGL 库", 87706, "#4CAF50"),
@@ -73,7 +73,7 @@ FLASH_CODE_SUB = [
("FatFS", 8468, "#9C27B0"),
("C 库 (microlib)", 5486, "#795548"),
("startup", 468, "#B0BEC5"),
- ("其他 (长尾)", 7557, "#E0E0E0"), # remainder(2026-10-23 S-5 C3/C1/C2 的零散增量)
+ ("其他 (长尾)", 7585, "#E0E0E0"), # remainder(2026-10-23 S-5 C3/C1/C2 的零散增量)
]
# ── Task stack budgets (2026-10-22 @build + 同日真机 [STACK] 实测; data/stack_report_20261022.md) ──
@@ -246,7 +246,7 @@ def gen_flash_svg():
SCALE = (W - 80) / FLASH_TOTAL
svg = svg_header(W, H)
- svg += f'Flash (1MB) 使用布局 — 2026-10-22 @build\n'
+ svg += f'Flash (1MB) 使用布局 — 2026-10-23 @build\n'
x = BAR_X
for label, size, color in FLASH_CATEGORIES: