修复了一个malloc的问题

This commit is contained in:
2026-09-18 17:46:46 +08:00
parent 2553f30fb8
commit acd29d76d9
9 changed files with 941 additions and 37 deletions
+17 -7
View File
@@ -376,6 +376,13 @@ int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len)
int cfg_store_stat(const char *p_path, cfg_file_stat_t *p_st); /* 0 = 存在 / -1 = 缺失/失败 */
int cfg_store_remove(const char *p_path);
/* 平台堆(P0-1,2026-09-18):共享核心的 KB 级临时缓冲一律经此三件套取用,
* 核心禁止直接调用 C 库 malloc —— MCU 端 = pvPortMalloc/vPortFree(FreeRTOS 堆),
* 模拟器端 = malloc/free(宿主 CRT 堆,堆余量以 MB 计,不参与 MCU 预算) */
void *cfg_store_alloc(size_t size); /* 失败返回 NULL,由调用方打印 FAIL */
void cfg_store_free(void *p_buf); /* NULL 安全;只接受 cfg_store_alloc 返回值 */
uint32_t cfg_store_heap_free(void); /* 空闲堆诊断量;模拟器恒 0 */
/* 路径由后端提供(MCU: /config.ini、/config.TMP、/cfg/netcfg.bin;
* 模拟器: sim_config.ini、sim_config.ini.tmp、sim_netcfg.bin) */
const char *cfg_store_config_path(void);
@@ -436,20 +443,22 @@ void adc_cfg_init(void)
## 8. 内存预算
依据:最新 map(2026-09-13):静态区(128 KB)已用 125,104 B(95.4%),**余 5,968 B**;FreeRTOS 堆 48 KB,稳态占用 35.4 KB,**空闲 ≈13.4 KB**。
依据:最新 map(2026-09-18,P0-1 修复后):静态区(128 KB 映射 SRAM1)已用 **126,064 B(96.2%)**,**余 5,008 B**;FreeRTOS 堆 `configTOTAL_HEAP_SIZE = 49,152 B`,扣除六任务栈 32 KB + 空闲/定时器栈 + IPC 后**空闲 ≈14 KB**(首启实测值直接看串口 `load: OK (…B, heap free …B)` 行)。
| 项 | 大小 | 性质 | 说明 |
|---|---|---|---|
| 段 RAM 副本 | **0 增量** | 稳态 | 复用 `net_cfg` 既有 `s_cfg`(22 B,.data) |
| 段表 / 键表 | ~150 B | 稳态(Flash) | const,进 .constdata |
| 段注册表(上限 8 段) | ~350 B | 稳态(.bss) | 含键行号表 256 B / 变更快照 64 B / 基线 16 B(实施实测) |
| REENTRANT 互斥锁 | ~100 B | 稳态(堆) | 2 个 osMutex |
| 读缓冲 | ≤ 4 KB | 暂态(堆) | `cfg_file_load`/`save` 期间(输入复用读缓冲) |
| 写出缓冲 | ≤ 5 KB | 暂态(堆) | `cfg_file_save` 外科手术输出;溢出 = 放弃保存(原文件不动) |
| **堆峰值(save 期间)** | **≈ 9 KB** | 暂态 | < 空闲 13.4 KB ✓ |
| **静态 .bss 增量** | **≈ 0.7 KB(实测 706 B)** | 稳态 | 段表 + 行号表 + 快照 + 基线 << 静态余量 5.9 KB ✓ |
| REENTRANT 互斥锁 | ~100 B | 稳态(FreeRTOS 堆) | 2 个 osMutex |
| 读缓冲 | ≤ 4,097 B | 暂态(**FreeRTOS 堆**) | `cfg_store_alloc`;`load`/`save`/`poll` 期间 |
| 写出缓冲 | ≤ 5,120 B | 暂态(**FreeRTOS 堆**) | 外科手术输出;溢出 = 放弃保存(原文件不动) |
| **堆峰值(save 期间)** | **≤ 9,217 B** | 暂态 | 编译期由 `CFG_HEAP_BUDGET = 10,240 B` 钉死,放大缓冲宏会先编译失败 |
| **静态 .bss 增量** | **≈ 0.7 KB(实测 706 B)** | 稳态 | 段表 + 行号表 + 快照 + 基线 << 静态余量 5.0 KB ✓ |
**结论:无需内存优化专项**(LVGL 池迁 CCM 等);实施后以 map 复核两个余量数字。
**P0-1(2026-09-18)实现纠偏**:本表原设计即"暂态走堆",但实现落在 C 库 `malloc` 上——本机工具链为 **MicroLIB**,其 C 堆只有 `Heap_Size = 0x200`(512 B),真机上 4 处 KB 级分配**必然返回 NULL**,症状是"配置读不到 → 静默回落工厂默认值"(模拟器用宿主 CRT 堆,故两端表现不一致)。现全部改走 `cfg_store_alloc`;map 复核:`malloc.o` 不再参与链接,`Removing startup_stm32f407xx.o(HEAP), (512 bytes)` → **静态区反而净增 512 B 余量**。
**结论**:配置引擎自身不需要内存优化专项;但 9,217 B 暂态峰值已占 FreeRTOS 堆空闲的一半以上,且与 lftpd 的每命令分配共存——**流式读写(免整文件驻留内存)仍列为后续优化项**,方案见《项目审查与优化建议报告》§4.4.1 与 §5.3。
## 9. 兼容与迁移
@@ -574,3 +583,4 @@ void adc_cfg_init(void)
| 2026-09-14 | v1.8:**P3 完成**(双端 0/0):`cfg_file` 段键表引擎 + `cfg_file_store` 双端后端 + `net_cfg` [network] 段集成(对外 API 不变,D14 兑现)+ `net_cfg_legacy` 旧档迁移;无头实测通过(工厂模板生成 / 编辑回读 / 非法键工厂回退 / 未知段容留 / 迁移三态 / 双端 0 警告);§6 接口修订为实现版(store 带路径参数 + 三路径 getter;变更回调随段注册,撤销独立 `cfg_file_register_cb`);§8 静态增量按实测 706 B 修订 |
| 2026-09-14 | v1.9:**P5 完成**(sim 0 警告):模拟器主循环接入 2s `cfg_file_poll`(与 MCU netTask 同周期);新增无头自检 `lvgl_sim.exe --cfgtest`(加载/迁移 → 建基线 → 伪造外部编辑 → 两次轮询沉降 → 断言 RAM 更新 + 外科保存回环校验注释/值无损);修复其暴露的两处缺陷:① MSVC `rename()` 不覆盖已存在目标(C1117)——模拟器后端先删目标再 rename(目标丢失时引擎自动重建模板兜底);② **外科保存截断 bug**——`s_split_line` 将 CRLF 行的 `\r`/`\n` 双双 NUL 化,旧 `s_restore_newlines` 只复原一个 `\n` 即遇 NUL 终止,保存产物只剩首行(P3 未触发"对已存在文件保存"故未暴露)——改为 Pass 1 用一次性副本(堆峰值维持约 9KB)、删除复原函数 |
| 2026-09-15 | v1.10:**配置引擎属主任务变更** netTask → defaultTask(P1-3 跨任务竞态修复,P1-5 一并落地)——① 上电加载移入 defaultTask 的 STORAGE_READY 屏障之前(屏障语义 = 卷挂载 + 配置就绪);② UI 保存改跨任务交接:`net_cfg_save_request()`("待保存"标志 + `SYS_EVT_CFG_SAVE` 事件唤醒)→ defaultTask `net_cfg_save_run()` 执行写盘、成功触发"立即应用"(`SYS_EVT_NET_RECONFIG` → netTask 重建),保存行随交接状态显示 ENTER/WAIT/OK/ERR(写盘期间的新请求重位置位不丢失);③ 2s poll 收敛 defaultTask——引擎静态状态仅单任务触碰,竞态在结构上消除;④ netTask 只读 RAM 副本、不触碰配置文件引擎;D15-③ 单写者、§5.1~5.4 时序图与并发矩阵同步;双端构建 0/0、UI 五步门禁全绿 |
| 2026-09-18 | v1.11:**P0-1 修复(临时缓冲改走平台堆)**——共享核心 4 处 `malloc`(读缓冲 4,097B / 模板输出 5,120B / 外科保存 Pass1 副本 + 输出 5,120B)此前落在 **MicroLIB C 堆**上,而该堆由 `startup_stm32f407xx.s` 的 `Heap_Size EQU 0x200` 限定为 **512 B**,真机必然分配失败 → 配置加载静默回落工厂默认(模拟器用宿主 CRT 堆,两端不一致,故双端 0 警告 0 错也照不出来)。① `cfg_file_store` 新增 `cfg_store_alloc/cfg_store_free/cfg_store_heap_free` 后端三件套(MCU = `pvPortMalloc/vPortFree`,与 `lftpd_io.c` 同模式;模拟器 = `malloc/free`),共享核心不再出现 C 库分配符号;② 新增编译期堆峰值预算 `CFG_HEAP_BUDGET = 10240B`(typedef 负数组实现,ARMCC 5 C99 无 `_Static_assert`),放大 `CFG_FILE_MAX_LEN`/`CFG_OUT_MAX` 直接编译失败;③ 四条失败路径(load 读失败 / 模板写失败 / 保存分配失败 / 保存写盘失败)增 `syslog_post(SYSLOG_SELFTEST, …)` 上报,不再只留串口行;`load: OK` 行附带 `heap free` 实测值;④ 属主任务注释同步为 defaultTask。map 复核:`malloc.o` 不参与链接、`Removing startup_stm32f407xx.o(HEAP), (512 bytes)`,静态区 126,584 → **126,064 B**(余 4,488 → **5,008 B**);双端 0 错 0 警、`--cfgtest` PASS |
+750
View File
@@ -0,0 +1,750 @@
# STM32F4-Base 项目审查与优化建议报告
| 项目 | 内容 |
|------|------|
| 审查日期 | 2026-09-18 |
| 代码基线 | `HEAD = 2d0ec35`(工作树干净,98+ 提交) |
| 固件基线 | `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`,构建于 2026-09-16 12:33,0 Error / 0 Warning |
| 审查范围 | 仓库全量(2,013 个受版本控制文件);重点为自研代码 24,353 行(108 文件)与构建产物内存画像 |
| 审查方式 | 静态审查 + 链接图(.map)量化分析 + 构建脚本/门禁核对;**未做上机动态验证**(除特别标注外) |
| 目标 | 结构合理性、文档分布合理性、模块设计合理性、改进路线图、内存优化、UI 标准化 |
> **与既有文档的关系**:本报告不重复 `docs/项目审计改进清单.md`(逐条缺陷台账)与 `docs/内存管理/RAM使用分析与优化方案.md`(内存专项方案),而是做**体系层评审 + 量化补强 + 新增发现**。新增发现统一编号 `N-xx`,已在台账中的条目按 `P1-x/P2-x/P3-x` 引用。
> **落地进展(2026-09-18)**:**N-01 已修复**(L0 方案,台账 `P0-1`)——`cfg_file_store` 后端新增 `cfg_store_alloc/cfg_store_free/cfg_store_heap_free`,MCU 端落到 FreeRTOS 堆(`pvPortMalloc`),共享核心 C 库分配符号清零 + 编译期堆峰值预算 + 四条失败路径上报自检记录。双端 0 错 0 警、`--cfgtest` PASS;map 复核 `malloc.o` 已不参与链接且 `Removing startup_stm32f407xx.o(HEAP), (512 bytes)`,**SRAM1 由 126,584 B / 96.6%(余 4,488 B)降到 126,064 B / 96.2%(余 5,008 B)**。本报告其余内存数字仍以修复前的 2026-09-16 map 为基线,逐条换算时扣 512 B 即可;真机 `@test` 正向确认仍待执行。
---
## 0. 执行摘要
### 0.1 六维评分
| 维度 | 评分 | 一句话结论 |
|------|------|-----------|
| 项目结构 | **B+** | 六层分层真实可执行、命名一致、门禁到位;主要问题是 **BSP 层承载业务模块**、**双构建清单漂移**、**5 个源文件两边都不编译** |
| 文档分布 | **B-** | 工程文档密度与深度高于同级嵌入式项目;主要问题是 **文档占仓库 60% 体积(其中芯片手册图片 40.5 MB)**、**README 任务表 4× 过期**、**约 18 条断链**、**文档承诺了不存在的工具/台账** |
| 模块设计 | **B** | 配置引擎、网络串行化、就绪屏障、FatFs 互斥等设计成熟;主要问题是 **失败路径普遍"死循环/静默降级"**、**无看门狗**、**FTP 无鉴权**、**cfg_file 依赖注定失败的 malloc** |
| 内存使用 | **C** | SRAM1 已用 96.6%(余 4,488 B),**64 KB CCM 完全空转**,FreeRTOS 堆被 6 条任务栈吃掉 2/3,**栈/堆水位零观测** |
| Flash 使用 | **B+** | 仅用 23.4%;但 **≈50 KB(20% 镜像)是可证明的死代码**(LVGL 配置漂移所致) |
| UI 标准化 | **B-** | 已有"声明源→生成器→表驱动"的正确骨架;缺**设计令牌层**、**页面原型库**、**注册自动化**、**golden 回归**,42 个菜单叶子仅 7 个有内容页 |
### 0.2 必须优先处理的问题(Top 6)
| # | 编号 | 问题 | 影响 | 状态 |
|---|------|------|------|------|
| 1 | **N-01** | `App/cfg_file.c` 用 C 库 `malloc(4097/5120)`,而本工程 MicroLIB C 堆只有 **512 B**(`startup_stm32f407xx.s:43 Heap_Size EQU 0x200`,map 中 `HEAP` ZI = `0x200`) | 真机上配置**加载/模板生成/外科式保存全部失败**并静默回落工厂默认;PC 模拟器用宿主 CRT 堆 → 掩盖问题 | ✅ **已修复(2026-09-18,L0;台账 P0-1)**,真机 `@test` 正向确认待跑 |
| 2 | **N-02** | `lftpd.c:367-371 cmd_pass` 对任意口令无条件回 `230` | 局域网内任意主机可读写 `/config.ini`(含 IP/MAC/网关)→ 可远程断网;单连接 CH395F 上还可挤掉 UI/对时服务 | 代码级确凿 |
| 3 | **N-03** | 无独立看门狗(IWDG);`configASSERT` 为 `taskDISABLE_INTERRUPTS(); for(;;);`;多处初始化失败即 `for (;;)` 挂死 | 现场死机不可自恢复,且断言时**连日志都没有** | 全局 |
| 4 | **N-04** | SRAM1 余 4,488 B(P0-1 修复后 5,008 B)而 **CCM 64 KB 未使用**;**栈水位零观测**(`uxTaskGetStackHighWaterMark` 全库零调用);堆水位原本被链接器整体裁掉,随 P0-1 修复已由 `cfg_store_heap_free` 引入(`load: OK …, heap free NB` 行开始打印实测值) | 任何新功能都会撞 `L6406E`;无法判断栈是过大还是不足 | 量化见 §5 |
| 5 | **N-05** | `Lib/lv_conf.h` 与 LVGL 9.3 漂移:设了不存在的 `LV_USE_THEME_BASIC`,未设 `LV_USE_THEME_SIMPLE`(默认 1)→ 运行时强链 `lv_theme_simple` 并把 label/buttonmatrix/textarea/bar 拉进镜像;`LV_DRAW_SW_SUPPORT_*` 全默认 1 | ≈**50 KB 死代码**(35,310 B 非 1bpp 混合核 + 13,389 B 未用控件 + 866 B 主题) | 见 §5.4 |
| 6 | **N-06** | 5 个 `.c` 既不在 uvprojx 也不在 simulator(`adc_task.c`/`crc.c`/`lftpd_log.c`/`ringbuf.c`/`rs485_task.c`) | "写了不编译"= 死代码 + 误导;`crc`/`ringbuf` 是 RS485 协议的前置件却无人注册 | 见 §2.3 |
### 0.3 三条主线建议
1. **把内存从"卡死"变"富裕"**:启用 CCM 承载 LVGL 池与 UI 帧缓冲(≈54 KB),同时补齐栈/堆水位自检,作为后续功能的预算来源(§5.3)。
2. **把失败路径从"死循环"变"可恢复"**:IWDG + 任务心跳位图 + 统一的"降级但活着"策略(§4.1)。
3. **把 UI 从"每页手写"变"声明 + 原型库"**:令牌层 → 原型库 → 生成器补全注册 → golden 回归,使新增一页的成本从 8 处手工改动降到 1 处声明(§6)。
---
## 1. 审查方法与证据基线
### 1.1 数据源
| 数据 | 来源 | 可信度 |
|------|------|--------|
| 内存/Flash 占用 | `.map` 的 component 表(214 个含码对象,列序 `Code incData RO RW ZI Debug Name`) | 高(构建产物,权威) |
| 总量 | map 尾部 `Total RO / Total RW / Total ROM` + `Program Size` | 高 |
| 依赖方向 | 逐文件 `#include` 扫描 + map 的 `refers to` 交叉表 | 高 |
| 构建注册 | `MDK-ARM/STM32F407-Demo.uvprojx` + `simulator/CMakeLists.txt` + git 文件清单三方对齐 | 高 |
| 文档链接 | 全量 md 链接解析(剔除设备路径 `PB12`/通配/占位后 706 → 94 未解析 → 人工分诊) | 中高(人工分诊) |
### 1.2 复现命令(本报告所有数字可重跑)
```powershell
# 1) 构建体量
Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern 'Total RO Size|Total RW Size|Total ROM Size'
# 2) MicroLIB C 堆真实大小(应为 0x200 = 512 B)
Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern '__heap_base|__heap_limit|HEAP'
# 3) 对象级 RW+ZI 排行(本报告脚本:obj_sizes2.py,注意 map 组件表是 6 个数值列)
# 4) 三方文件注册对齐(uvprojx / CMakeLists / git)
# 5) 仓库体量:git ls-files -z | 逐个统计字节(必须用 -z,否则中文名会被控制台代码页破坏)
```
> ⚠️ **踩坑记录**(供后续审查复用):
> ① map 组件表是 **6 列数值**(含 `incData` 与 `Debug`),按 5 列解析会得到 0 个对象;
> ② `git ls-files` 输出经 PowerShell 管道会按控制台代码页破坏中文路径,导致"文件不存在"的假结论,必须用 `-z` + 字节流解码;
> ③ 终端渲染的中文乱码不等于文件损坏,判定损坏须统计 `EF BF BD` 字节数(本报告统计结果:**0**,全库无 U+FFFD)。
### 1.3 口径定义
| 术语 | 定义 |
|------|------|
| 自研代码 | `App/`、`Drivers/BSP/`(不含 `Lib/`、`Middlewares/`、HAL/CMSIS)、`test/`;不含 CubeMX 生成的 `Src/`、`Inc/` |
| Flash 占用 | `Code + RO-data`(=245,384 B);`ROM` 再加 `RW-data` 初始化表(=246,016 B) |
| RAM 占用 | `RW-data + ZI-data` = 126,584 B,上限 128 KB(0x20000000);**CCM 64 KB 不在 scatter 内,计 0** |
| "死代码" | 该对象在镜像中有码,但其功能被工程配置(如 `LV_COLOR_DEPTH 1`)或调用点扫描(零调用者)证明不可达 |
---
## 2. 项目结构合理性
### 2.1 规模与分布(实测)
| 分类 | 文件 | 行数 | 说明 |
|------|------|------|------|
| `App/`(自研应用) | 18 | 3,852 | 配置引擎、墙钟、日志、任务薄壳 |
| `Drivers/BSP/`(自研驱动/子系统) | 62 | 16,369 | 其中 **UI 子系统 5,875 行**(含 `ui_font_cn_12.c` 1,211) |
| `test/`(自研测试) | 13 | 4,132 | 13 个测试模块 |
| **自研合计** | **108** | **24,353** | |
| CubeMX `Src/`+`Inc/` | 22 | 4,286 | 受 USER CODE 标记保护 |
| `simulator/` | — | 2,292 | PC SDL2 宿主 |
| 第三方 | — | ≈55 万行 | LVGL 9.3(uvprojx 编译 201 个 `.c`)、FreeRTOS、FatFs、dhara、lftpd |
**评价**:自研体量与第三方体量比约 1:20,属于典型的"薄自研 + 厚中间件"。这决定了**结构审查的重点不在自研代码内部,而在"自研如何约束第三方"**——而 §5.4 会证明这层约束(`lv_conf.h`)目前是失守的。
### 2.2 分层与依赖方向
实测自研层间的**全部反向依赖**(BSP → App)只有 4 处:
| 位置 | 反向 include | 判定 |
|------|-------------|------|
| `Drivers/BSP/NETCFG/net_cfg.c:28` | `cfg_file.h` | 可接受但需说明:NETCFG 实为"业务配置模块",放在 BSP 层是历史命名 |
| `Drivers/BSP/NETCFG/net_cfg_legacy.c:19,21` | `cfg_file.h`、`cfg_file_store.h` | 同上 |
| `Drivers/BSP/UI/ui_page_runlog.c:19` | `sys_log.h` | UI 读日志属正常消费者,但 `sys_log` 在 App 层 → 层次倒置 |
| `Drivers/BSP/UI/ui_page_selftest.c:19` | `sys_log.h` | 同上 |
| `Lib/FatFs/ffsystem.c` | `sys_clock.h`(App/util) | 中间件→应用;已在文件内注释声明为"④中间件层"有意为之 |
**评价:良好**。在 24k 行规模上把反向依赖控制在 4 处,说明分层不是墙上的图。但存在**语义层错位**:
- `Drivers/BSP/NETCFG/`、`Drivers/BSP/UI/` 名字是"板级支持包",内容是**业务**(网络配置语义、菜单/页面/权限/内容页框架)。后果:`sys_log`(App)与 `net_cfg`(BSP)互相引用,层次图自相矛盾。
- **建议(S-1)**:把 `NETCFG`、`UI` 在概念上重新定性为 `App/` 或新增 `Service/` 层,`Drivers/BSP/` 只留下与引脚/芯片强绑定的 `ch395f`、`gd5f2gq5ue`、`sd2506`、`tpafe5160`、`rs485`、`dbg_*`。迁移成本最低的做法:**先改文档与分层图(零风险),目录迁移留到有 CI 兜底之后**(避免与 §2.3 的注册问题叠加)。
依赖方向裸引用(绕过头文件契约的 `extern`)实测 4 处:
| 位置 | 裸 extern |
|------|-----------|
| `Drivers/BSP/NET/ch395f.c:143` | `extern volatile uint8_t g_spi1_dma_done;`(实为 GD5F 的变量,跨模块反向) |
| `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c:43` | `extern SPI_HandleTypeDef hspi1;` |
| `Drivers/BSP/NET/net_socket.c:51` | `extern osMessageQueueId_t netMsgQueueHandle;` |
| `Drivers/BSP/sd2506/sd2506.c:17` | `extern I2C_HandleTypeDef hi2c1;` |
**建议(S-2)**:CubeMX 句柄类(`hspi1`/`hi2c1`)统一由 `main.h` 导出并 include,删除局部 extern;`g_spi1_dma_done` 应由 GD5F 模块自己定义并在头文件 `extern` 声明(当前跨模块裸 extern 是真正的耦合隐患);`netMsgQueueHandle` 改为 net_socket 内部持有 + 提供投递 API。
### 2.3 双构建清单漂移(结构性风险最高的一条)
把 **git 文件清单 × uvprojx × simulator/CMakeLists** 三方对齐后:
| 类别 | 文件 | 后果 |
|------|------|------|
| **两边都不注册** | `App/task/adc_task.c`、`App/util/crc.c`、`App/util/ringbuf.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c`、`App/task/rs485_task.c` | 永不编译 → 死代码;`crc.c`/`ringbuf.c` 是 RS485 帧协议前置件,无人注册即无人使用;`lftpd_log.c` 与 `lftpd_log.h`(现为 DBG 宏)语义重复 |
| **仅 simulator 注册** | `cfg_file_store_sim.c`、`net_socket_sim.c`、`sim_port.c`、`sim_syslog.c`、`ui_net_sink_backend_sim.c` | 正常(后端适配层模式) |
| **uvprojx** | 290 个 `.c`(含 201 个 LVGL) | LVGL 全量编译 → 编译时长;死代码问题被"全编"放大(见 §5.4) |
**评价**:AGENTS.md 已把"新文件必须注册两处"写成铁律并有 `new-source-file` skill,但**缺少反向校验**("存在但未注册")。这是纯粹的机械检查缺口。
**建议(S-3,1 小时工作量)**:新增 `scripts/check_registration.py`,规则:
1. `App/**.c`、`Drivers/BSP/**.c`(排除 `*_sim.c`、`*_mcu.c` 之外的模式白名单)必须同时出现在 uvprojx 与 CMakeLists;
2. 例外必须写进 `scripts/registration_allowlist.txt` 并注明原因(如"待 RS485 协议启用");
3. 挂入 pre-commit 的 `local` hooks(与 `no-mojibake` 同列)。
这样 §2.3 的 5 个文件会立刻变成"要么注册、要么显式豁免"。
### 2.4 目录组织与根目录卫生
| 观察 | 证据 | 判定 |
|------|------|------|
| 根目录散置工作文件 | `待处理内容.md`(仅 2 行的 RAM 待办)、`ui_menu_tree.txt`、`ui_cn_chars.txt` | 菜单树是**生成器输入**,放根目录合理但需在 `docs/` 有权威说明;`待处理内容.md` 内容已被 `docs/内存管理/` 覆盖 → 建议合并后删除 |
| `Lib/` 命名 | `Lib/` 下混放 `lvgl/`、`lv_conf.h`、`FatFs/`、`dhara/` | `lv_conf.h` 是**工程配置**不是库文件;建议移到 `Config/lv_conf.h` 并同步 include 路径(低风险、提升可读性) |
| 功能开关名存实亡 | `App/app_cfg.h` 的 `APP_NET_ENABLE/APP_ADC_ENABLE/APP_RS485_ENABLE` **全库零引用** | 与台账 **P2-12** 同源。开关不生效比没有开关更糟——评审者会误信"ADC 可裁剪" |
| 测试任务无条件创建 | `Src/freertos.c:171` | 与台账 **P2-13** 同源;产品镜像白耗 4 KB 栈 |
| 未使用的 CCM | `.sct` 只有 `ER_IROM1` + `RW_IRAM1 0x20000000 0x20000` | 见 §5 |
### 2.5 结构结论
```mermaid
graph TD
A[app_main_init CubeMX] --> B[osKernelStart]
B --> T1[defaultTask 512w<br/>FS挂载+配置引擎属主]
B --> T2[netTask 1024w<br/>net_poll+消息串行]
B --> T3[uiTask 4096w<br/>LVGL+UI sink]
B --> T4[ftpTask 1024w]
B --> T5[timeSyncTask 512w]
B --> T6[testTask 1024w<br/>产品构建也在跑]
T2 -.netMsgQueue 深8.-> T2
T3 -.10ms 轮询 accept.-> T2
T4 -.等待.-> E1[NET_READY & STORAGE_READY]
T5 -.等待.-> E1
```
**总体判断:结构合理,无需重构**。要做的是四类**低成本纠偏**:S-1 层次定性、S-2 消裸 extern、S-3 注册反向校验、S-4 让 `app_cfg.h` 开关真正生效或删除(并把 `APP_CCM_ENABLE`/`APP_UI_NET_SINK` 纳入同一处)。
---
## 3. 文档分布合理性
### 3.1 体量画像(实测,工作树)
| 对象 | 文件数 | 体积 | 占仓库 |
|------|--------|------|--------|
| 仓库受控文件总计 | 2,013 | **78.3 MB** | 100% |
| `docs/` 合计 | 652 | **47.19 MB** | **60.3%** |
| └ `docs/芯片手册/images` | 564 张 JPG | **40.5 MB** | **51.7%** |
| └ `docs/images/引用关系图.json` | 1 | 1,052 KB | 1.3% |
| └ `docs/UI 设计/mockups` | 15 | 1,110 KB | 1.4% |
| `.git` pack | — | 46.6 MB | — |
| 工程文档(Markdown,自研) | 40 | 14,124 行 | — |
| 外部资料(Markdown,datasheet 转录) | 16 | 59,142 行 | — |
**关键失衡**:工程文档 14,124 行 vs 外部资料 59,142 行;**图片体量是全部 Markdown 的 3 倍**。这意味着:
- `git clone` 的净耗时几乎全部花在扫描手册图片上;
- 与工程决策相关的信息(40 篇)在体积上被完全淹没;
- `引用关系图.json`(1 MB)是**生成产物**,与 `.dot/.svg` 一并提交(AGENTS.md 明确要求),但它对阅读无价值、对 diff 有害。
### 3.2 做得好的(应保留并显式制度化)
1. **文档密度与可执行性**:每篇都带"步骤 + 判据"(如 UI 制作流程有 P0→P4 Gate);测试说明按模块拆分并给出预期串口标记,可直接接 `@test`。
2. **陷阱台账**:`docs/问题记录/*_Trap_Records.md` + `docs/项目审计改进清单.md`(P1-1..P3-13,带 ✅ 状态与日期)——这是本工程最有价值的资产,多数嵌入式项目没有。
3. **图与脚本同提交**:`tools/arch_diagram/`、`tools/refgraph/` 生成器 + 产物入库,图可再生而非手绘。
4. **UI 设计资产链完整**:原始参考图 → 说明 md → 代码,可追溯。
### 3.3 问题清单
#### D-1(P1)外部资料与工程文档同仓同树
**影响**:clone 慢、备份慢、code review 时 diff 噪音大;`docs/芯片手册/` 的 564 张图属于**只读输入资料**,与代码演进无关。
**建议**(三选一,按代价递增):
1. **最省**:把 564 张 JPG 降采样到长边 1,200 px(40.5 MB → 约 8 MB),一次性提交并在 `docs/文档规范.md` 写入"入库图片上限 200 KB/张";
2. **推荐**:`docs/芯片手册/` 拆为独立仓库 `dtu-datasheets`,用 `git submodule` 挂回原路径(现有全部链接不需改写);
3. **最正**:Git LFS(需要托管端支持)。
#### D-2(P1)README 任务表 4× 过期,与 AGENTS.md 冲突
`README.md:71-76` 仍写 `defaultTask 2048 words / netTask 4096 / uiTask 16384`,而 `Src/freertos.c:64-105` 与 `AGENTS.md` 均为 `512/1024/4096`。台账 **P2-16** 只修了 AGENTS.md,README 漏修。
**建议**:
- 立即修正(一次性);
- **根治**:任务表由脚本从 `Src/freertos.c` 解析生成,README/AGENTS 引用同一生成块(`scripts/gen_task_table.py` 输出 Markdown 片段,pre-commit 校验一致性)。这与 §7 的"文档-代码一致性门禁"合并实施。
#### D-3(P1)约 18 条断链(目录搬迁与工具未落地导致)
分诊后**真实失效**清单(附录 B 有完整列表):
| 类型 | 代表位置 | 说明 |
|------|---------|------|
| 目录搬迁遗留 | `docs/测试说明/{FTP,GD5F2GQ5UE,SD2506}_Test_Guide.md`、`测试开发标准流程.md:358`、`问题记录/CH395F_Trap_Records.md:434` | 仍指旧路径(`docs/` 根下) |
| 路径片段错误 | `docs/代码规范/cppcheck使用说明.md:3`、`docs/配置文件设计说明.md:159/160/494/568-570` | 相对层级算错或文件已改名 |
| **指向不存在的工具** | `UI_界面制作流程.md:413`、`UI_自动化测试流程.md:104` → `tools/ui_mock/` | **文档承诺了未实现的 UI 渲染/比对工具**(§6.5 golden 回归的前置件) |
| **指向不存在的产物** | `docs/内存管理/RAM使用分析与优化方案.md:172/279/136` → `scripts/ram_baseline.tsv`、`docs/内存管理/RAM预算台账.md`、`App/util/app_mem_stats.*` | 方案写了、工具没建 → 读者按文档执行会失败 |
| 目标路径变更 | `图形绘制流程.md:135`(真实目标在 `docs/images/`)、`串口助手设计说明.md:200` | |
**建议**:新增 `scripts/check_doc_links.py`(校验仓库内相对链接与代码引用路径),挂 pre-commit;对"计划中的文件"统一用 `[计划]` 前缀标记而非直接链接,避免门禁长期红灯。
#### D-4(P2)孤儿文档
`docs/内存管理/内存使用分析规划.md` 无任何入链(项目自有文档中唯一孤儿)。**建议**:并入 `RAM使用分析与优化方案.md` 的"规划"章节,或在 `docs/README.md` 索引中显式列出。
#### D-5(P2)缺一个文档入口/索引
`docs/` 根下平铺 12 篇 md + 8 个子目录,无 `docs/README.md` 索引;权威入口实际是 `AGENTS.md`(面向 agent)与 `docs/项目架构总览.md`。**新成员的阅读顺序不可见**。
**建议**:新建 `docs/README.md`,按"上手 → 架构 → 模块 → 测试 → 规范 → 台账 → 外部资料"给出有序清单(约 60 行即可),并从 README.md 链接过去。
#### D-6(P2)命名与目录约定不统一
中文目录名含空格(`docs/UI 设计/`)在脚本/链接/CI 中易出错(本工程已出现多处需引号包裹的路径);同一目录混用 `UI_界面制作流程.md`(下划线+中文)与 `内容页架构.md`(纯中文)与 `RemoDispBus协议与接收设计说明.md`(中英连写)。
**建议**:确立 `docs/文档规范.md`:目录名一律**无空格**(`UI设计` 或 `ui`)、文件名"主题词+类型"、`snake_case` 用于英文段;新增文档必须出现在 `docs/README.md`。存量文档按 D-3 修链接时**顺带**重命名,一次做完。
#### D-7(P2)文档与代码之间没有一致性门禁
D-2/D-3 的共同根因:**没有任何机械检查**保证"文档说的 = 代码做的"。
**建议**:建立三档一致性检查(详见 §7 交付物):
| 检查 | 覆盖 | 手段 |
|------|------|------|
| 存在性 | 文档里的路径/文件/脚本必须存在 | `check_doc_links.py` |
| 数值 | 任务栈/优先级/端口/引脚表与代码一致 | `gen_task_table.py` + 端口常量表提取 |
| 体积 | 文档中引用的"内存/Flash 数字"与最新 map 一致 | 复用 `scripts/ram_report.py` 输出 |
### 3.4 文档结论
**内容质量高、分布形态差**。按 D-1..D-7 处理后可从 B- 提升到 A-:工程量约 1 人日(不含拆仓),收益是 clone 时间与"文档可信度"。
---
## 4. 功能模块设计评审
### 4.1 启动与任务框架
**设计良好**:
- `app_main_init` 顺序有因(墙钟优先,使后续日志带时间戳);
- **Event Flags 就绪屏障**(`NET_READY/STORAGE_READY/FTP_READY/TIMESYNC_READY/CFG_SAVE/NET_RECONFIG`)取代魔法延时,是本项目最漂亮的设计之一;
- 配置引擎属主收敛到 defaultTask 单任务(P1-3 已修),消除了跨任务竞态。
**问题**:
| 编号 | 问题 | 证据 | 影响/建议 |
|------|------|------|-----------|
| **N-03a** | 无 IWDG(全库无 `MX_IWDG_Init`/`HAL_IWDG_Refresh`) | grep 零命中 | 死机不可恢复。建议 1 s 超时 + 各任务在自身循环置心跳位图,defaultTask 校验"全部任务在 3 s 内动过"才喂狗 |
| **N-03b** | `Inc/FreeRTOSConfig.h` 的 `configASSERT(x)` = `taskDISABLE_INTERRUPTS(); for(;;);` | `:154` | 断言时**连串口都不输出**。建议改为:`DBG_ERROR` + 记录 `pcTaskGetName(xTaskGetCurrentTaskHandle())` 后再死循环(或直接触发 IWDG 复位) |
| **N-03c** | 多处初始化失败即挂死(`Error_Handler()` at `main.c:221`;net/ui 初始化失败路径) | | 建议统一降级策略:**外设失败 → 记自检日志 + 该子系统禁用 + 其余照常**(LED/屏上给出降级标识) |
| **N-07** | `configCHECK_FOR_STACK_OVERFLOW` 与 `configUSE_MALLOC_FAILED_HOOK` **均未开启** | `Inc/FreeRTOSConfig.h` 无此二宏 | 栈溢出/分配失败静默破坏内存。开销仅每任务几十字节 + 每上下文切换若干指令,**强烈建议立刻开启**(这是 §5 一切内存工作的安全网) |
| N-08 | 无 `HardFault_Handler` 自研覆盖(用 startup 默认死循环) | `Src/*.c` 无 | 建议加:把 `SP/PSP/LR/PC/xPSR` + 栈帧 `R0-R3,R12,LR` 打到串口(或写进 CCM 保留区,复位后可读),现场故障可定位 |
| P2-13 | testTask 无条件创建(`Src/freertos.c:171`) | | 产品镜像白耗 4 KB;改 `#if defined(TEST_SUITE_*)` 包裹 |
| P2-12 | `App/app_cfg.h` 三个 `APP_*_ENABLE` 零引用 | | 开关要么生效要么删 |
### 4.2 网络子系统(CH395F + BSD 抽象层)
**设计良好**:
- 所有 `net_*` 经 `netMsgQueue`(深 8,`Src/freertos.c:154`)串行到 netTask 执行 → 单线程语义清晰;
- 全部 CH395F 中断集中在 `net_poll()` 内 `GET_GLOB_INT_STATUS` 排空(AGENTS 铁律,已落实);
- 网络代次号机制使"改 IP 免重启"能重建各监听。
**问题**:
| 编号 | 问题 | 证据 | 建议 |
|------|------|------|------|
| **N-02** | **FTP 无鉴权**:`cmd_pass` 无条件 `send_simple_response(socket, 230, STATUS_230)` | `lftpd.c:367-371`(`cmd_user :539` 亦不校验) | 最小可用方案:`config.ini` 增 `[ftp] user/pwd_hash`,`cmd_pass` 用**定长比较 + 失败计数锁定**;生产固件默认关闭 FTP 写权限(`FF_FS_READONLY` 式的只读模式) |
| N-09 | `cmd_pasv` 每次新建监听 `lftpd_inet_listen(0)`(随机端口) | `lftpd.c:373` | 在**单连接 socket** 上与 UI `:7003`、对时 `:8888` 争用同一物理 socket;建议限定监听资源池 + 显式"FTP 会话独占"策略,并在文档写明"一次只允许一个 TCP 服务活动" |
| N-10 | uiTask 以 **10 ms** 周期向 netMsgQueue 投递 accept 轮询 → ≈100 msg/s,与 FTP 共享深度 8 的队列 | `ui_net_sink.c` 轮询逻辑 | 队列满时 `osErrorResource` → UI 丢帧/重投。建议:把"服务就绪"改为事件驱动(netTask 在 LISTEN 状态变化时置位),UI 侧改为等待事件;或给队列扩容并在 UI 侧背压 |
| P2-11 | `net_task.c` 对 `osEventFlagsGet` + Clear 非原子 | 台账 | 用 `osEventFlagsAcquire`/单次 Wait 消 TOCTOU |
| P2-8 | `net_recv` 阻塞超时 30 s | 台账 | 缩短并让上层可取消 |
| — | PHY 强制 100M 全双工,忽略自协商 | AGENTS/architecture | 与某些交换机会联不通;至少加"链路 up 失败 3 次后回退自协商" |
| P3-x | `NET_MAX_SOCKETS 8`(`net_config.h:24-25`)是**软件表**,与硬件单连接语义不符 | | 文档已说明,但 API 层看不到限制 → 建议在 `net_socket.c` 断言"同时 LISTEN 数 ≤ N" |
### 4.3 存储子系统(GD5F2GQ5UE + dhara + FatFs)
**设计良好**:
- 分层正确:`gd5f2gq5ue`(芯片)→ `nand_ftl`(dhara map/journal + 页缓存)→ FatFs R0.14 → 应用;
- `Lib/FatFs/ffsystem.c` 用真实 `osMutexNew` 实现 `FF_FS_REENTRANT=1` 的卷锁(多数移植是空实现)——**这是本工程移植质量的一个亮点**;`get_fattime()` 接系统墙钟,使 FAT 时间戳有意义;
- BBT 保留块池(`gd5f2gq5ue.c:28-40`)把坏块表存储区从 dhara 可见空间中排除,避免被 FTL 擦写。
**问题**:
| 编号 | 问题 | 证据 | 影响/建议 |
|------|------|------|-----------|
| **N-11** | NAND 驱动**直接对调用方缓冲做 DMA**:`HAL_SPI_Receive_DMA(&hspi1, p_buf, size)`(`:200`)、`HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)p_buf, size)`(`:258`),阈值仅 32 B | `gd5f2gq5ue.c:185-217,246-270` | **这是 CCM 迁移的硬边界**:所有走 NAND 的缓冲区(含 `FIL`、FatFs window、任务栈上的临时缓冲)**必须留在 SRAM1**。必须在 `docs/内存管理/` 写成铁律,否则一旦把某任务栈搬进 CCM 就会静默 DMA 失败 |
| **N-12** | `dhara_nand_is_free()` 只检查页首 64 B | `nand_ftl.c:137-154` | dhara 依赖该判定做映射恢复;页首 64 B 全 FF 但后半页有数据时会误判为空闲 → 数据丢失风险(低概率但后果重)。建议整页扫描或至少首/中/尾三处采样 |
| **N-13** | `dhara_nand_copy()` 未处理 `DHARA_NO_PAGE` 语义(dst 为哨兵值时仍执行"读+编程") | `nand_ftl.c:174-186` | dhara map 在回收/擦除路径会用 `DHARA_NO_PAGE` 试探;当前实现会把它当作真实页号 → 未定义行为。建议:`if (dst == DHARA_NO_PAGE) { dhara_set_error(err, DHARA_E_UNSUPPORTED); return -1; }`(返回不支持是安全语义) |
| N-14 | 写回缓存 `s_cache_buf/s_cache_dirty` 无掉电保护策略:`CTRL_SYNC` 已实现(`:343-353`)但**没有周期性或掉电前的强制 flush** | `nand_ftl.c:291-353` | 掉电窗口 = 最后一次未跨页的写。建议:空闲 N ms 由 defaultTask flush + 掉电检测(若硬件支持)或至少"日志落盘后立即 fsync" |
| P2-3/P2-4 | 部分返回值丢失 | 台账 | 逐条修 |
| N-15 | 页缓冲重复:`nand_ftl` 3×2,048(`:s_page_buf/s_cache_buf/s_copy_buf`)+ `gd5f` 的 `s_bbt_page_buf[2048]`(仅 init 期使用) | 对象 ZI:nand_ftl 6,216 + gd5f 2,596 | BBT 页缓冲可与 `s_copy_buf` 复用或 init 后归还(§5.3 R-3,省 2–4 KB) |
| — | `disk_ioctl GET_BLOCK_SIZE = 1` | `nand_ftl.c` | FatFs 的擦除块提示失效(FAT32 对齐优化拿不到信息);建议返回 64 页(=1 物理块) |
### 4.4 配置引擎(`App/cfg_file.c`,1,190 行)
**设计良好**:描述符 + 段注册表、外科式(surgical)保存、模板生成、首启旧档迁移、2 s 轮询(`cfg_file_poll`)响应 `SYS_EVT_CFG_SAVE`,属主单任务化。这是全项目设计最完整的一个模块。
**问题**:
| 编号 | 问题 | 建议 |
|------|------|------|
| **N-01(P0)** | **4 处 `malloc` 必失败**:`:485 malloc(CFG_FILE_MAX_LEN+1u)` = 4,097 B;`:522 malloc(CFG_OUT_MAX)` = 5,120 B(`CFG_OUT_MAX 5120`,`:44`);`:1038`/`:1050` 外科式保存两处。而 MicroLIB C 堆只有 **512 B**(`startup_stm32f407xx.s:43 Heap_Size EQU 0x200`;map:`HEAP` ZI `0x200` @`0x2001e878`,`__heap_limit 0x2001ea78`;`malloc.o refers to startup_stm32f407xx.o(HEAP)`)。map 交叉表确认 `cfg_file.o(i.s_load_file)/(i.s_write_template)/(i.cfg_file_save)` → `malloc.o(i.malloc)` | 见 §4.4.1 |
| P2-1 | 超长键行处理 | 台账 |
| — | 失败即静默回落工厂默认(`:955-988`:缺文件→模板→return -1;超长→模板→return -1;读失败→"factory defaults kept") | 语义本身合理(不阻塞组网),**但与 N-01 叠加后变成"永远出厂 IP"**,用户改了 IP 重启就丢——这正是最难排查的现场故障形态 |
#### 4.4.1 N-01 修复方案(推荐 A,2 小时)
| 方案 | 做法 | 评价 |
|------|------|------|
| **A(推荐)** | `cfg_file.c` 的 4 处 `malloc/free` 改 `pvPortMalloc/vPortFree`(FreeRTOS 堆 48 KB);并在 `CFG_FILE_MAX_LEN`/`CFG_OUT_MAX` 旁加 `_Static_assert` 约束峰值 | 与已完成的 `lftpd_io.c` 迁移一致(map 中 `lftpd_io.o refers to heap_4.o(i.pvPortMalloc)` 已证明该模式可用)。**注意**:48 KB 堆余量仅约 14–15 KB,9 KB 级分配需与 §5.3 的栈/池调整一并规划,否则会把"必失败"变成"偶发失败" |
| B(最稳) | 改**静态缓冲**:`static uint8_t s_cfg_in[CFG_FILE_MAX_LEN+1]` + `s_cfg_out[CFG_OUT_MAX]`。属主已收敛到 defaultTask 单任务 → 无并发风险 | 但当前 SRAM1 只剩 4,488 B,**放不下 9 KB** → 必须与 §5.3 的 CCM 迁移或 LVGL 池收缩一起做 |
| C | 提高 `Heap_Size` 或 retarget `_sys_heap` 到 `pvPortMalloc` | 512 B 是 MicroLIB 的兜底(`printf` 内部也会用);单独放大不解决"两套堆"的根本问题 |
> **实施记录(2026-09-18,按方案 A 落地,两处必要调整)**:① 分配接口**不写在 `cfg_file.c` 里,而是加到 `cfg_file_store` 后端**(`cfg_store_alloc` / `cfg_store_free` / `cfg_store_heap_free`)——`cfg_file.c` 是双端共享源码,直接写 `pvPortMalloc` 会让 PC 模拟器断链;MCU 后端 = `pvPortMalloc/vPortFree`,模拟器后端 = `malloc/free`。② 编译期峰值约束改用 **typedef 负数组**(`CFG_HEAP_BUDGET = 10,240 B`)——ARMCC 5 只到 C99,**没有 `_Static_assert`**(方案 A 原文此处需修正)。③ 另补 4 条失败路径的 `syslog_post(SYSLOG_SELFTEST, …)` 与 `load: OK …, heap free NB` 实测行。台账:`docs/项目审计改进清单.md` **P0-1**;设计文档:`docs/配置文件设计说明.md` §6.2/§8 + v1.11。
> **诚实标注**:本结论由静态分析得出(映射表 + 启动文件 + 调用图三重证据),**尚未上机确认**。`test/verify_log.ps1:34-35` 的 Forbidden 标记含 `FAIL`,而失败路径会打印 `read: malloc 4097B FAIL` / `template: malloc 5120B FAIL`,因此**一次 `@test` 即可判定**(若近期 `@test` 曾 PASS,需先确认当时是否真的进入了文件读取分支,例如 `/config.ini` 不存在或大小为 0)。**(2026-09-18 已按上述实施;修复后的真机 `@test` 正向确认——`load: OK` 出现且 `FAIL` 零命中——仍待执行。)**
### 4.5 日志子系统(`App/log/sys_log_mcu.c`)
**设计良好**:定长 64 B 记录(`SYSLOG_REC_SIZE`)、单写者(defaultTask)、RAM 队列 + `syslog_drain`、容量满时删除重建、`/log` 目录惰性创建。
**问题**:
| 编号 | 问题 | 证据 | 建议 |
|------|------|------|------|
| **N-16** | **每条记录一次 `f_open + f_write + f_close`**(`:89-95`),读侧同样每条 `f_open/f_lseek/f_close`(`:118-126`) | 每条日志触发 FatFs 目录项读写 → 2–4 次 NAND 页读写;UI 翻记录时逐条 open/seek/close | 写侧:常驻 `FIL`(仅 defaultTask 持有)+ 批量 flush;读侧:打开一次 + 缓存目录偏移(UI 已有 `s_cache_valid/s_last_rec` 雏形,可升级为页缓存) |
| P2-9 | `mkdir` 失败后不再重试(`s_fs_ready` 逻辑,`:158-165`) | 台账 | 失败时保持未就绪 + 定期重试 + 上报自检 |
| P2-10 | 容量轮转 `f_unlink`(`:64`)与 UI 读并发 | 台账 | 轮转只在写者、读者持版本号 |
### 4.6 UI 子系统
见 §6 专题(同时是"标准化"章节)。此处仅记设计层结论:三层拆分(数据 `ui_menu_tab` / 视图 `ui_menu_view` / 控制器 `ui_menu`)+ 内容页描述符框架**方向正确**,但**框架未收敛到"新页面零框架改动"**,且 4 份 3,200 B 帧副本占 12.8 KB RAM(§5)。
### 4.7 外设驱动
| 模块 | 评价 | 问题 |
|------|------|------|
| `sd2506`(RTC) | I2C 超时齐备,降级策略明确(RTC 故障→默认时基 + 自检上报),**良好** | P2-5(负温度解析)、P2-6(WRTC 解锁) |
| `tpafe5160`(并行 ADC) | BUSY 超时、EXTI 读取路径齐备 | **P1-1:采样结果无人消费(数据路径死)**;并行总线无 DMA/批量采样框架,16 位端口读依赖 `gpio.c` 初值 |
| `rs485` | 结构简单 | DE 时序用 `HAL_Delay(1)` **阻塞 1 ms**(在调用任务里);`crc.c`/`ringbuf.c` 未注册 → 无帧校验、无接收环 |
| `ch395f` | 事务包裹 `begin/end`、SPI 分频 8 为 PCB 约束(已文档化,勿动) | 静态 DMA 缓冲 2×2,052 B(obj ZI 4,105),必须留 SRAM1 |
**驱动共性缺口**:没有统一的"错误码 + 超时 + 重试 + 自检上报"约定,各模块自定返回码(`GD5F_OK` / `0` / `bool` 混用)。**建议(S-5)**:定义 `bsp_err_t` 统一错误码 + `bsp_retry()` 辅助 + 所有失败必须走 `syslog(SYS_SELFTEST,...)`,作为驱动编写规范写入 `docs/代码规范/`。
### 4.8 测试体系
**设计良好**:`test/test_config.h` 的 `TEST_SUITE_*` 互斥(`#error` 保护)、CH395F 硬件层/NET 层 Phase 互斥、`@test` 一键闭环(烧录→wait-idle→标记校验)、模拟器 `--cfgtest/--logtest`。
**问题**:
| 编号 | 问题 | 建议 |
|------|------|------|
| P1-10 | CH395F 硬件测试套件 RAM 溢出(`L6406E`):HW 套 +8.7 KB、NET 层固件 +28 KB(`s_rx_buf` 32 KB) | 测试缓冲改为**分 Phase 复用同一块静态缓冲**(union 或分阶段 `memset`);`待处理内容.md` 的这条待办应升级为正式任务 |
| P2-14 | `build.bat` 用子串匹配判定 PASS → 假通过 | 改为解析 `"0 Error(s), 0 Warning(s)"` 精确串 + 退出码双条件 |
| P2-15 | `download.bat` 可能读旧日志 | 烧录前删除/打时间戳 |
| **N-17** | **无栈/堆水位断言**:测试通过不代表栈余量安全 | 在每个测试套件收尾打印 `uxTaskGetStackHighWaterMark(NULL)` + `xPortGetFreeHeapSize()`,把"通过"变成"通过且有余量"(配合 §5.2 的自检任务) |
| N-18 | 无 CI(仅本地 pre-commit);无覆盖率概念 | 至少加一条 GitHub Actions/本地脚本:`cppcheck --all` + `build.bat`(若可 headless)+ `check_registration.py` + `check_doc_links.py` |
---
## 5. 内存优化(量化)
### 5.1 实测基线(map,2026-09-16)
| 指标 | 数值 | 上限 | 占用 |
|------|------|------|------|
| `Code + RO-data` | 245,384 B(239.6 KB) | 1 MB Flash | **23.4%** |
| `ROM`(含 RW 初始化表) | 246,016 B | 1 MB | 23.5% |
| `RW-data` | 632 B | — | — |
| `ZI-data` | 125,952 B | — | — |
| **SRAM1(0x20000000,128 KB)** | **126,584 B** | **131,072 B** | **96.6%(余 4,488 B)** |
| **CCM(0x10000000,64 KB)** | **0 B** | 65,536 B | **0%** |
`.sct` 只声明了 `ER_IROM1` + `RW_IRAM1 0x20000000 0x00020000 { .ANY (+RW +ZI) }`;文件头注释说明"未使用 SRAM3(克隆片可能没有)"——但**CCM 是 F407 片上必定存在的 64 KB**,与克隆风险无关(GD32F407 亦有 CCM)。
**RW+ZI 主要对象(实测 obj ZI/RW)**
| 对象 / 符号 | 字节 | 归属 | DMA 可达性 |
|-------------|------|------|-----------|
| `ucHeap`(`heap_4.c`,含 `heap_4.o` 合计 49,184) | 49,184 | FreeRTOS 堆 | 任意 |
| `work_mem_int`(`lv_mem_core_builtin.o`,= `LV_MEM_SIZE 40*1024`) | 40,960 | LVGL 池 | **CPU-only(可进 CCM)** |
| `ui_port.o`:`g_ui_fbuf[3200]` + `g_ui_draw_buf[3200]` | 6,412 | UI 帧/绘制缓冲 | 可进 CCM |
| `ui_net_sink.o`:`g_tx[4+4+3200+1]` + `g_lcdmem_data[4+3200]` + `g_rx[64]` | 6,762 | UI 网络发送 | 可进 CCM(CH395F 侧 memcpy 到自己缓冲) |
| `nand_ftl.o`(3×2,048 页缓冲 + 控制块) | 6,216 | 存储 | **必须 SRAM1** |
| `ch395f.o`(`s_spi2_dma_tx/rx[2052]`) | 4,105 | 网络 | **必须 SRAM1** |
| `gd5f2gq5ue.o`(`s_bbt_page_buf[2048]` + BBT 表) | 2,596 | 存储 | **必须 SRAM1** |
| `cmsis_os2.o` | 1,724 | RTOS 胶水 | 任意 |
| `startup_...o`:`STACK 0x400` + `HEAP 0x200` | 1,536 | MSP + MicroLIB 堆 | 任意 |
| `tasks.o`(含 `pxReadyTasksLists[56]` = 1,120) | 1,280 | RTOS | 任意 |
| `freertos.o` | 1,104 | 任务薄壳 | — |
| `default_fatfs`(FATFS,含 `win[512]`,定义于 `Src/freertos.c`,计入 `freertos.o` 的 1,104 B) | ≈560 | FatFs 卷对象 | **必须 SRAM1** |
| `lftpd_io.o`(`s_ftp_file` FIL) | 556 | FatFs 文件句柄 | **必须 SRAM1** |
| `cfg_file.o` | 724 | 配置引擎 | 任意 |
| `lv_init.o`(`lv_global`) | 496 | LVGL | 任意 |
| `net_socket.o` | 304 | 网络 | 任意 |
**两个静态池(49,184 + 40,960 = 90,144 B)就占了整个 128 KB 的 71%**——这是问题的核心。
**Flash 侧(Code+RO,214 个含码对象)**
| 子系统 | Code+RO | 占镜像 |
|--------|---------|--------|
| LVGL(含核心/绘制/控件/字体/缓存/图像) | ≈150,600 | **61.5%** |
| 手写 UI 页面(`ui_*.o`,5,875 行) | 17,520 | 7.2% |
| 网络(NET + NETCFG + lftpd) | 18,245 | 7.4% |
| HAL/CMSIS | 11,866 | 4.8% |
| 应用基础(App/ + Src/) | 10,468 | 4.3% |
| FreeRTOS + CMSIS-RTOS | 8,011 | 3.3% |
| FatFs + dhara + NAND | 8,222 | 3.4% |
### 5.2 结构性诊断(四条)
1. **64 KB CCM 空转**,而 96.6% 的负载压在 128 KB SRAM1 上。
2. **任务栈全部动态取自 FreeRTOS 堆**:`osThreadAttr_t` 只设 `stack_size` 不设 `stack_mem`(`Src/freertos.c:64-105`)→ 6 条栈 **32,768 B** + idle/timer 栈 + TCB + IPC 全部从 48 KB 池里扣,**留给动态分配的实际余量仅约 14–15 KB**。这也是为什么 N-01 若改走 `pvPortMalloc` 仍需先扩容/腾挪。
3. **零观测**:`xPortGetFreeHeapSize`/`xPortGetMinimumEverFreeHeapSize` 在 map 中只以"linker removed"出现(从未被调用),`uxTaskGetStackHighWaterMark` 全库零调用,`lv_mem_monitor` 仅在 `ui_menu_init` 打过一次。**无法回答"还能加多少"**。
4. **MicroLIB 512 B C 堆是隐形地雷**:任何新代码写一个 `malloc` 都会中招(N-01 已是实例)。
### 5.3 RAM 优化路线(收益/风险/优先级)
| 编号 | 措施 | 收益 | 风险 | 优先级 |
|------|------|------|------|--------|
| **R-1** | **开启 `configCHECK_FOR_STACK_OVERFLOW = 2` + `configUSE_MALLOC_FAILED_HOOK = 1`**(钩子里打印任务名 + 触发 IWDG 复位) | 0(安全网) | 极低;每任务 +几十 B、切换略增开销 | **P0,立即** |
| **R-2** | **启用 CCM**:`.sct` 增 `RW_IRAM2 0x10000000 0x00010000 { *(CCM_DATA) *(.bss.CCM_DATA) }`;对 LVGL 池用 `LV_MEM_ATTR __attribute__((section("CCM_DATA")))`,UI 四份帧缓冲同样加段属性;用 `APP_CCM_ENABLE` 宏包裹(克隆片兜底 = 全部回 SRAM1) | **−54,122 B(52.9 KiB)** = 40,960 + 6,400 + 6,762 | 中:必须确保这些缓冲**不被任何 DMA 直接寻址**。已核实 UI 发送路径 memcpy 进 CH395F 自有 SRAM1 DMA 缓冲(`ch395f.c:1132-1190`),LVGL 为纯 CPU;**NAND 路径(N-11)绝对不可迁入** | **P1,本季度** |
| **R-3** | 存储页缓冲合并:`gd5f.s_bbt_page_buf` 与 `nand_ftl.s_copy_buf` 复用(BBT 仅在 init 期使用);`nand_ftl` 三块 2 KB 中 `s_page_buf`(dhara scratch) 与 `s_copy_buf` 不可并发 | **−2 ~ −4 KB** | 中:需严格审查 dhara 调用序列(copy 期间不会用 map scratch) | P2 |
| **R-4** | UI 帧副本去重:同一 3,200 B 帧当前存 **4 份**(`g_ui_draw_buf` → `g_ui_fbuf` → `g_lcdmem_data` → `g_tx`)。让 `ui_flush_cb` 直接把 `color_p` 交给 sink、sink 在发送时才拼帧头(就地写 8 B 头 + 复用同一 buffer) | **−6.4 ~ −9.6 KB** | 中:需要 sink 与 LVGL 刷新生命周期对齐(同任务,可控) | P1 |
| **R-5** | `configMAX_PRIORITIES` 56 → 32(实际最高用到 `osPriorityHigh` 级别) | **−480 B** | 低 | P2 |
| **R-6** | 任务栈改**静态分配**(`stack_mem` 指向各自 `.bss` 数组),从堆里释放 32,768 B | 堆可用 +32 KB(**总量不变**,但把"池内碎片"变成"可核算静态区") | 低-中:静态后溢出检测更难发现;建议配合 R-1 先测水位再定尺寸 | P2 |
| **R-7** | testTask 门控(P2-13) | **−4 KB** | 低 | P1 |
| **R-8** | 观测常开:新增 `memStatsTask`(最低优先级,1 Hz,512 w)打印 `xPortGetFreeHeapSize` / `xPortGetMinimumEverFreeHeapSize` / 各任务高水位 / `lv_mem_monitor`,并支持串口命令按需打印 | 0(可核算) | 低 | **P1**(是 R-2/R-6 的前置) |
| R-9 | `LV_MEM_SIZE` 40 KB → 依 R-8 实测峰值下调(既有方案预估 16 KB) | −16 ~ −24 KB | 低(有 R-8 兜底) | P2 |
**组合结果预估(注意措施之间存在重叠,不可简单相加)**
| 组合 | SRAM1 变化 | 说明 |
|------|-----------|------|
| 基线 | 126,584 B / **96.6%** | 余 4,488 B |
| + R-2(CCM 启用) | **72,462 B / 55.3%** | −54,122 B;CCM 用 54,122 B(仍余 11 KB) |
| + R-7(testTask 门控) | **68,366 B / 52.2%** | −4,096 B |
| + R-3(存储页缓冲合并) | **≈66.3 KB / 50.6%** | −2,048 ~ −4,096 B |
| + R-5(优先级 56→32) | **≈65.8 KB / 50.2%** | −480 B |
> ⚠️ **R-2 与 R-4 不可叠加计算**:R-4(UI 帧副本 4→2)省的是"副本份数",R-2 已把这四份缓冲整体搬到 CCM,因此 **R-4 在 R-2 之后省的是 CCM 而非 SRAM1**(CCM 侧 13,162 B → 6,762 B,为后续 UI 扩展腾出 6.4 KB CCM 空间)。若只做 R-4 不做 R-2,则直接为 SRAM1 省 6.4 KB——两条路线对 RAM 总量等价,**但顺序决定收益落在哪个区域**:先做 R-2 时,R-4 应重新定性为"CCM 优化"。
最终形态:**SRAM1 约 50%、CCM 约 20% 使用率**,`待处理内容.md` 里记的 `L6406E`(P1-10 测试镜像溢出)随之消解,并为 ADC 数据路径(P1-1)、RS485 协议栈(`crc.c`/`ringbuf.c` 注册)预留出预算空间。
> 与 `docs/内存管理/RAM使用分析与优化方案.md` 的关系:本表**沿用其阶段划分**(阶段 0 测量 / 阶段 1.1 池收缩 / 阶段 1.2 CCM / 阶段 2 堆收缩),新增的是 ①R-4 帧副本去重(原文档未识别 4 份副本)②R-3 存储侧缓冲合并 ③N-11 的 **DMA 可达性硬边界**(原文档未提出,属实施风险关键点)④R-1/R-8 的观测前置条件。
### 5.4 Flash 优化(≈50 KB 可证明死代码)
**根因:`Lib/lv_conf.h` 与 LVGL 9.3 漂移。** 该文件手写裁剪过(126 行),但:
- 设了 `LV_USE_THEME_DEFAULT 0` 与**已不存在的** `LV_USE_THEME_BASIC 0`(`:70-71`);
- **未设 `LV_USE_THEME_SIMPLE`** → `lv_conf_internal.h:2606` 默认 1 → `lv_display.c:121-125` 在建 display 时执行 `disp->theme = lv_theme_simple_init(disp)`;
- `lv_theme_simple.o(i.theme_apply)` 引用 `lv_label_class / lv_button_class / lv_buttonmatrix_class / lv_bar_class / lv_textarea_class` → 这些控件全被拉进镜像;
- 而 map 与源码扫描双重证明:**工程 UI 只创建 `lv_obj`**(8 处 `lv_obj_create`),**零控件构造函数**、**零 flex/grid 用法**(`ui_*.c` 无 `set_flex/set_grid/LV_LAYOUT`);密码页也是 `lv_draw_label/lv_draw_rect` 自绘。
| 编号 | 措施 | 可省 Flash | 依据 |
|------|------|-----------|------|
| **F-1** | `LV_DRAW_SW_SUPPORT_I1 1`,其余 `LV_DRAW_SW_SUPPORT_L8/RGB565/ARGB8888/X8888/ALPHA` 等显式设 0 | **35,310 B** | `lv_draw_sw_blend_to_*` 合计 40,582 B,其中 `to_i1` 仅 5,272 B;`LV_COLOR_DEPTH 1` 下其余混合核不可达 |
| **F-2** | `LV_USE_THEME_SIMPLE 0` + 关闭未用控件 `LV_USE_LABEL/BUTTON/BUTTONMATRIX/BAR/TEXTAREA/SLIDER/ARC/TABLE` | **14,255 B** | 控件 13,389 B(label 4,087 + buttonmatrix 3,707 + textarea 3,680 + bar 1,843 + button 72)全部由 `lv_theme_simple.o`(866 B) 锚定 |
| **F-3** | `LV_USE_FLEX 0`、`LV_USE_GRID 0` | **5,368 B** | 源码扫描零使用;注释"菜单依赖 flex/label/button"已过期(自绘后不再依赖) |
| **F-4** | 关闭 `LV_USE_TRANSITION/LV_USE_SNAPSHOT/LV_USE_OBJ_PROPERTY/LV_USE_FLOAT`(若为 1)、动画与 `lv_obj_style` 中未用部分 | 需实测 | `lv_draw_sw_transform` 4,340 / `mask` 3,862 / `box_shadow` 3,246 / `draw_sw_img` 2,960 / `bin_decoder` 1,972 + `image_decoder` 952 / `triangle` 930 / `grad` 420 —— 工程无图片、无阴影、无变换 |
| **F-5** | 从 uvprojx 中**移除未使用的 LVGL 控件源文件**(201 个 → 实际需要的子集) | 编译时长 −40% 左右 | 与 F-2 配合;`lv_conf` 关开关后文件仍编译,删文件才省编译时间 |
| **F-6** | 双精度软浮点:`dadd/dmul/ddiv/depilogue/dfixul` 合计 **1,018 B**(`mf_w.l`),来自 `printf` 的 `%f`/double 提升 | −1 KB + 运行期性能 | 日志与 UI 全用定点;把 `%f` 改整数刻度,或编译期加 `-D__MICROLIB` 下 `printf` 浮点裁剪选项 |
**合计可回收 ≈ 50–60 KB Flash(镜像的 20%+)**,且**不减少任何现有功能**。这是"低风险高收益"的一类改动,建议与 R-1 同批做(先 Flash 后 RAM,避免同时改两处难定位)。
### 5.5 内存治理长效机制
1. **`scripts/ram_report.py` 升级为门禁**:超阈值(如 SRAM1 > 92%)即失败,并在 PR 描述里自动附 Top-10 对象表。
2. **建立 `scripts/ram_baseline.tsv`**(`docs/内存管理/RAM使用分析与优化方案.md:172` 已承诺但未创建):每次构建记录 Code/RO/RW/ZI/Top 对象,形成趋势。
3. **RAM 预算台账**(同上,文档已承诺 `docs/内存管理/RAM预算台账.md`):按子系统配额(网络 12 KB / 存储 12 KB / UI 16 KB / RTOS 48 KB / 应用 8 KB),超配额必须先申请。
4. **DMA 可达性清单**(新增,源于 N-11):文档化"哪些缓冲必须 SRAM1",并在 CCM 迁移时对该清单加 `_Static_assert`/地址断言(`assert((addr & 0xFFF00000) == 0x20000000)`)。
5. **禁止新增 C 库 `malloc`**(源于 N-01):写进 `docs/代码规范/`,并在 `check_coding_standard.py` 加正则检查(`App/`、`Drivers/` 内出现 `malloc(` 即告警)。
---
## 6. UI 界面标准化实现方案
### 6.1 现状(框架已有,但未"闭环")
| 层 | 现状 | 评价 |
|----|------|------|
| 声明源 | `ui_menu_tree.txt`(缩进即层级,行尾 `#P` 标密码) | ✅ 正确的方向 |
| 生成器 | `tools/menu_gen/gen_menu.py` → `ui_menu_tab.c/.h`(42 节点,`{parent,pwd,name}`) | ✅ 已闭环 |
| 视图 | `ui_menu_view.c` 自绘行 + 按需建毁框 | ✅ 1bpp 下的正确取舍 |
| 控制器 | `ui_menu.c:181-232 menu_key_handle` | ✅ |
| 内容页 | `ui_content_page.c` 描述符表 `s_defs[]` **仅 7 页**(DEVICE_INFO/AC/DC/PASSWORD/NET_CFG/RUN_LOG/SELFTEST_LOG) | ❌ **42 个菜单叶子中仅 7 个有内容页**,`menu_content_page_for()`(`ui_menu.c:155/:205`)是**手写 switch 映射** |
| 字体 | `tools/font_gen/make_font.py` + `ui_cn_chars.txt`(自动生成,文件头已声明"请勿手改") | ✅ 闭环 |
| 设计令牌 | **无**(每页自带 `CP_SCREEN_W/H 160`、`CP_TITLE_Y 2`、`CP_PAGE_LINE_Y 143`、`CP_ROW_BUF_LEN 32` 等布局常量) | ❌ 魔法数字分散在各页 |
| golden 回归 | 文档指向 `tools/ui_mock/`(**不存在**) | ❌ 无视觉基线 |
### 6.2 新增一个页面要走多少步(当前)
1. `ui_menu_tree.txt` 加节点 → 2. 重跑 `gen_menu.py` → 3. 新建 `ui_page_xxx.c` → 4. 在 `ui_content_page.c` 的枚举/`s_defs[]` 注册 → 5. 在 `ui_menu.c:menu_content_page_for()` 手写 `case N:`(**下标硬编码,表序一变即错**)→ 6. 注册到 `MDK-ARM/*.uvprojx` → 7. 注册到 `simulator/CMakeLists.txt` → 8. 新汉字跑 `font-regen`。
**5 步是纯机械劳动且 2 处易错(步骤 5 的魔法下标、步骤 6/7 漏注册)。**
### 6.3 标准化目标:四层 + 一个闭环
```mermaid
graph LR
A[L0 设计令牌<br/>ui_tokens.h] --> B[L1 组件库<br/>ui_w_* 自绘控件]
B --> C[L2 页面原型<br/>list / detail / form / records]
C --> D[L3 页面实例<br/>ui_page_xxx.c 仅填数据]
D --> E[生成器 gen_page_registry.py]
E --> F[ui_page_registry.c<br/>菜单节点→页 映射]
F --> G[门禁: check_registration + golden]
```
**L0 设计令牌(1 天,零行为变化)** — 新建 `Drivers/BSP/UI/ui_tokens.h`:
```c
/* 屏幕与骨架 */
#define UI_SCREEN_W 160
#define UI_SCREEN_H 160
#define UI_TITLE_Y 2
#define UI_TITLE_H 11
#define UI_PAGE_LINE_Y 143 /* 页脚分隔线 */
#define UI_ROW_PITCH 13 /* 行距(12px 字体 + 1) */
#define UI_ROW_H 12
#define UI_LIST_TOP 15
#define UI_SCROLLBAR_W 2
#define UI_PAD_X 3
#define UI_BUF_STRLEN 32 /* = CP_ROW_BUF_LEN,统一一处 */
/* 语义色(1bpp:只有黑白,但保留语义名以便换深度时平移) */
#define UI_COL_FG LV_COLOR_BLACK
#define UI_COL_BG LV_COLOR_WHITE
#define UI_COL_SEL_FG UI_COL_BG
#define UI_COL_SEL_BG UI_COL_FG
/* 样式:只用 const 描述符(动态 lv_style 会每次泄漏 88 B,见 ui_content_page.c:100-104) */
extern const lv_style_const_prop_t UI_STYLE_FLAT[];
```
要点:**保留现有"只用 `lv_style_const_prop_t` 常量样式"的纪律**(这是已被验证的必要约束:动态样式每个 88 B 泄漏);令牌化后所有页面自动获得统一观感,改一行即全局生效。
**L1 组件库(3 天)** — 把 `ui_menu_view.c` / `ui_content_page.c` 中重复的自绘逻辑抽成无状态组件(纯 `lv_layer_t` 绘制,不创建 LVGL 对象,保持零控件、零池压力):
| 组件 | 职责 | 从何处抽取 |
|------|------|-----------|
| `ui_w_title` | 标题栏 + 分隔线 + 页码 | `ui_content_page.c` 通用绘制链 |
| `ui_w_list` | 行列表 + 选中反白 + 滚动条 | `ui_menu_view.c` 行绘制 |
| `ui_w_kv` | "名称: 值" 两列(装置信息/实时数据) | `ui_page_device_info.c`、`ui_page_ac.c` |
| `ui_w_records` | 定长记录分页浏览(含 `s_cache_valid/s_last_rec` 缓存范式) | `ui_page_runlog.c`、`ui_page_selftest.c`(两文件结构几乎相同 → 直接合并) |
| `ui_w_form` | 键值 + 光标 + 可编辑(含十进制/IP 输入约束) | `ui_page_net_cfg.c` |
| `ui_w_msgbox` | 确认/错误提示("密码输入错误"等) | `ui_page_password.c` |
**L2 页面原型(4 类,覆盖全部剩余页面)**:
| 原型 | 适用菜单 | 数据契约 |
|------|---------|---------|
| `detail`(KV 只读) | 装置信息、交流量、直流量、遥信量 | `kv_provider_t{ count(); get(i, name, val); }` |
| `form`(可编辑) | 整定/定值/控制字/软压板、遥测死区/系数、遥信类型/防抖/双点、装置维护、通讯参数、网口/SNTP | 直接绑定 `cfg_file` 描述符(**复用配置引擎的段注册表,无需新数据结构**) |
| `records`(分页日志) | SOE/事故/操作/保护告警/保护启动/遥控/自检/运行记录/运行报告 | `record_provider_t{ count(); get(idx, ts, text); }`(sys_log 已是这个形态) |
| `action`(确认型) | 强制复归、清除记录、手动录波 | 提示 → 二次确认 → 执行 → 结果日志 |
菜单树 42 个叶子中,**除已实现的 7 个外,其余 35 个可 100% 归入这 4 类原型**(form 21 个、records 9 个、detail 3 个、action 2 个)。这意味着**剩余页面工作量从"35 页 × 各自设计"降为"4 个原型 + 35 份数据绑定"**。
**L3 页面实例** — 目标形态(以"遥测死区"为例,全部新代码约 30 行):
```c
static const ui_form_item_t s_items_deadzone[] = {
/* cfg 段/键, 显示名, 类型, 最小, 最大, 权限 */
{ "deadzone", "ia", "A 相死区", UI_T_UINT, 0, 1000, UI_PERM_MODIFY },
{ "deadzone", "ib", "B 相死区", UI_T_UINT, 0, 1000, UI_PERM_MODIFY },
{ "deadzone", "ic", "C 相死区", UI_T_UINT, 0, 1000, UI_PERM_MODIFY },
};
const ui_page_desc_t ui_page_deadzone = UI_FORM_PAGE("遥测死区", s_items_deadzone);
```
**生成器补全(1.5 天,关键收益)** — 扩展 `ui_menu_tree.txt` 支持页绑定注解,让 `gen_menu.py` 一并产出注册表,**消灭步骤 4/5 的手工 switch**:
```
遥测死区 #P @page=deadzone @arch=form
```
产物:`ui_page_registry.c` 含 `node_index → (page_id, archetype, desc*)` 常量表 + `ui_page_registry_for(node)`;`ui_menu.c` 只需一次调用。生成器同时校验:`@page` 指向的描述符是否存在、是否有节点声明了页但无实例、是否有实例未被任何节点引用(编译期断言或生成期报错)。
**闭环门禁(1 天)**:
1. `scripts/check_registration.py`(同 §2.3 S-3)保证新页面两端都注册;
2. **golden 回归落地**(补上文档承诺的 `tools/ui_mock/`):利用现成的 **PC 模拟器**做渲染基线——`simulator` 增加 `--shot <page> <out.bin>`(已有 `--standby shot.bin` 先例)导出 1bpp 帧,Python 脚本转 PNG 并与 `docs/UI 设计/baseline/*.png` 逐像素比对,差异 > 0 即 FAIL。这样 `UI_界面制作流程.md:413` 与 `UI_自动化测试流程.md:104` 的引用由"空头承诺"变成可执行门禁;
3. 字符集自动校验:构建前扫描所有 `ui_*.c` 中的汉字,与 `ui_cn_chars.txt` 求差集,缺失即 FAIL(把 `font-regen` 从"记得跑"变成"漏跑就红")。
### 6.4 UI 侧的内存纪律(必须写进规范)
| 规则 | 依据 |
|------|------|
| 页面**不得**新增静态大缓冲;文本缓冲统一用 `UI_BUF_STRLEN` | 现有 `s_ts/s_desc` 已是范式 |
| 每页**只用** `lv_style_const_prop_t`,禁止运行期 `lv_style_init` | `ui_content_page.c:100-104` 已验证动态样式每次泄漏 88 B |
| 优先自绘(`lv_draw_*` + `lv_text_get_width`),新控件需评审(会破坏 §5.4 F-2 的裁剪) | LVGL 已占 Flash 61% |
| 帧缓冲任何改动必须遵守 R-4(副本数 ≤ 2) | 当前 4 副本 12.8 KB |
| `LVGL 池`用量必须经 `lv_mem_monitor` 实测后才允许扩大 | 现 40 KB,预估可降到 16 KB |
### 6.5 实施顺序(UI 专项,总计约 8 人日)
| 步骤 | 内容 | 交付判据 |
|------|------|---------|
| U1 | `ui_tokens.h` 令牌化(纯替换,不改行为) | `@build` PASS + 模拟器截图与替换前逐像素一致 |
| U2 | `ui_w_records`(合并 runlog/selftest) | 两页行为不变,代码 −1 个文件 |
| U3 | `ui_w_form` + `ui_page_desc_t` 原型化 + 生成器产注册表 | 手工 `menu_content_page_for()` 删除;新增一页只需"改树 + 加实例文件" |
| U4 | golden 回归(模拟器 `--shot` + PNG 比对)+ 字符集检查 + 注册检查入 pre-commit | 三个新 hook 全绿 |
| U5 | 用原型批量落剩余 35 页(每页 ≤30 行) | 菜单树全绿、无 `CONTENT_PAGE_COUNT` 哨兵命中 |
---
## 7. 统一改进路线图
### P0(1 周内,必须)
| 编号 | 事项 | 验收 |
|------|------|------|
| N-01 | `cfg_file.c` 4 处 `malloc` → `pvPortMalloc`(方案 A)或静态缓冲(方案 B,需先做 R-2) | 真机串口出现 `config loaded` 而非 `read: malloc 4097B FAIL`;`@test` PASS |
| N-02 | FTP 鉴权(或生产构建默认关闭 FTP 写) | 错误口令登录被 `530` 拒绝;`@test` 加 FTP 标记校验 |
| N-03 | IWDG + 心跳位图;`configASSERT` 改为"先打印后复位" | 人为死循环 → 1 s 内复位并留下复位原因日志 |
| N-07 | 开 `configCHECK_FOR_STACK_OVERFLOW=2` + malloc-failed 钩子 | 连续 30 min 运行无钩子触发(同时得到真实栈水位) |
| D-2 | 修 README 任务表 | 与 `Src/freertos.c` 一致 |
| N-06 | 5 个未注册 `.c`:注册或显式删除 + 白名单 | `check_registration.py` 零告警 |
### P1(1 个月内)
| 编号 | 事项 | 预期 |
|------|------|------|
| N-04 / R-2 | **CCM 启用**(LVGL 池 + UI 帧缓冲,`APP_CCM_ENABLE` 门控)+ N-11 DMA 边界文档与断言 | SRAM1 96.6% → ~55% |
| R-4 | UI 帧副本 4 → 2 | −6.4 KB |
| R-8 | `memStatsTask` / 串口命令 `mem` 常开 | 每次评审有数可依 |
| F-1/F-2/F-3 | `lv_conf.h` 与 LVGL 9.3 对齐 + 主题/控件/flex 裁剪 | Flash −55 KB |
| N-12/N-13 | `dhara_nand_is_free` 全页校验;`dhara_nand_copy` 处理 `DHARA_NO_PAGE` | 掉电注入测试通过 |
| N-16 | 日志常驻 `FIL` + 批量 flush;UI 记录读缓存升级 | 单条日志 NAND 操作次数下降 ≥50% |
| N-10 | UI accept 改事件驱动(脱离 100 msg/s 轮询) | 队列占用可测量下降 |
| D-1 | 芯片手册图片降采样 / 拆 submodule | clone 40 MB → ~8 MB |
| D-3 | `check_doc_links.py` 入门禁 + 修 18 条断链 | 零断链 |
| U1–U4 | UI 令牌化 + 原型库 + 生成器 + golden | 新增一页 ≤30 行 |
| P1-1/P1-7/P1-8/P1-10 | 沿用台账(ADC 数据路径 / MAC 写序 / 重建失败恢复 / 测试 RAM 溢出) | 台账闭环 |
### P2(一个季度内)
`R-3/R-5/R-6/R-9`、`F-4/F-5/F-6`、D-4/D-5/D-6/D-7、S-1/S-2/S-5、P2-1..P2-16 余项、N-17/N-18(测试内建水位 + CI 最小集)。
### P3(机会性)
UI 剩余页面批量落地(U5)、目录级层次迁移(S-1 的物理搬迁)、`app_cfg.h` 开关体系重构、`Lib/lv_conf.h` 迁至 `Config/`。
### 交付物清单(可直接排期)
| 交付物 | 类型 | 来源条目 |
|--------|------|---------|
| `scripts/check_registration.py` | 新脚本 + pre-commit hook | S-3 / N-06 |
| `scripts/check_doc_links.py` | 新脚本 + pre-commit hook | D-3 |
| `scripts/gen_task_table.py` | 文档一致性生成器 | D-2 / D-7 |
| `scripts/ram_baseline.tsv` + `docs/内存管理/RAM预算台账.md` | 文档承诺落地 | §5.5 |
| `Drivers/BSP/UI/ui_tokens.h` | 令牌层 | U1 |
| `Drivers/BSP/UI/ui_w_{list,kv,records,form,msgbox,title}.{c,h}` | 组件库(需双端注册) | U2/U3 |
| `tools/menu_gen/gen_menu.py` 扩展 + `ui_page_registry.c` 生成 | 生成器 | U3 |
| `tools/ui_mock/{render_mockup.py,i1_to_png.py}` + `docs/UI 设计/baseline/` | golden 回归 | U4 |
| `docs/内存管理/DMA可达性清单.md` | 规范(含"哪些缓冲禁止进 CCM") | N-11 |
| `docs/README.md`(文档索引) | 文档 | D-5 |
---
## 附录 A 关键证据索引
| 结论 | 证据 |
|------|------|
| SRAM1 96.6%、余 4,488 B | map:`Total RW Size (RW Data + ZI Data) 126584 (123.62kB)`;scatter `RW_IRAM1 0x20000000 0x00020000` |
| CCM 未使用 | `MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct` 仅两个区域,无 `0x10000000` |
| MicroLIB C 堆 = 512 B | map `:17405 0x2001e878 - 0x00000200 Zero RW HEAP startup_stm32f407xx.o`;`__heap_limit 0x2001ea78`;`MDK-ARM/startup_stm32f407xx.s:43 Heap_Size EQU 0x200` |
| cfg_file 走 C 库 malloc | map `cfg_file.o(i.s_load_file) refers to malloc.o(i.malloc)`、`(i.s_write_template)`、`(i.cfg_file_save)`;源码 `App/cfg_file.c:19 <stdlib.h>`、`:485/:522/:1038/:1050` |
| lftpd 已迁 FreeRTOS 堆 | map `lftpd_io.o refers to heap_4.o(i.pvPortMalloc)` |
| 堆/栈零观测 | 全库 grep `xPortGetFreeHeapSize|uxTaskGetStackHighWaterMark|configCHECK_FOR_STACK|configUSE_MALLOC_FAILED` 在 `App/Src/BSP/test` 零命中;map 中相关库函数标记为 linker-removed |
| 任务栈全部动态分配 | `Src/freertos.c:64-105` 的 `osThreadAttr_t` 无 `.stack_mem`;`freertos.o` ZI 仅 1,104 B |
| 非 I1 混合核死代码 35,310 B | map 对象 `lv_draw_sw_blend_to_*` 合计 40,582 − `to_i1` 5,272 |
| 未用控件 13,389 B 由主题锚定 | `lv_theme_simple.o(i.theme_apply) refers to lv_{label,button,buttonmatrix,bar,textarea}_class`;源码零控件构造函数(仅 8 处 `lv_obj_create`) |
| NAND 直接 DMA 调用方缓冲 | `gd5f2gq5ue.c:200`、`:258`(`GD5F_DMA_THRESHOLD = 32`) |
| CH395F memcpy 自有缓冲 | `ch395f.c:1132-1190` |
| FTP 无鉴权 | `Drivers/BSP/NET/lftpd/lftpd.c:367-371` |
| 端口分配 | `App/time_sync.c:25 TIME_SYNC_PORT 8888`;`Drivers/BSP/UI/ui_net_sink.h:29 UI_NET_SINK_PORT 7003`;FTP 21 |
| 42 菜单叶子 / 7 内容页 | `Drivers/BSP/UI/ui_menu_tab.c`(生成,42 节点);`ui_content_page.c:84-92 s_defs[]` |
| 手写映射 | `ui_menu.c:155/:205 menu_content_page_for()` |
| 文档体量 | 工作树字节统计(见 §3.1 表) |
## 附录 B 断链清单(P1 修复项)
| 文件:行 | 失效链接 | 处置 |
|---------|---------|------|
| `docs/代码规范/cppcheck使用说明.md:3` | 相对路径层级错误 | 改 `../` 层级 |
| `docs/配置文件设计说明.md:159,160,494,568,569,570` | 指向已改名/搬迁文件 | 逐项改指 `net_cfg.c`/`cfg_file_store.h` 等 |
| `docs/测试说明/FTP_Test_Guide.md`、`GD5F2GQ5UE_Test_Guide.md`、`SD2506_Test_Guide.md` | 目录搬迁遗留(旧 `docs/` 根路径) | 改新相对路径 |
| `docs/测试说明/测试开发标准流程.md:358` | 同上 | 同上 |
| `docs/问题记录/CH395F_Trap_Records.md:434` | 同上 | 同上 |
| `docs/UI 设计/UI_界面制作流程.md:413` | `tools/ui_mock/`(不存在) | 与 U4 一并实现,或标注 `[计划]` |
| `docs/UI 设计/UI_自动化测试流程.md:104` | 同上 | 同上 |
| `docs/内存管理/RAM使用分析与优化方案.md:136,172,279` | `App/util/app_mem_stats.*`、`scripts/ram_baseline.tsv`、`docs/内存管理/RAM预算台账.md`(均不存在) | 随 §5.5 创建;未创建前标 `[计划]` |
| `docs/测试说明/串口助手设计说明.md:200` | 目标路径变更 | 改指现路径 |
| `docs/图形绘制流程.md:135` | 目标实际在 `docs/images/` | 改路径 |
## 附录 C 评审中确认"无需改动"的设计(避免后续误改)
| 项 | 结论 |
|----|------|
| CH395F SPI 分频 = 8 | **PCB 约束,禁止"优化"**(AGENTS 铁律,实测一致) |
| 就绪屏障用 Event Flags 而非魔法延时 | 正确,保留 |
| FatFs `FF_FS_REENTRANT=1` + 真实互斥 | 正确,保留(多数移植偷工) |
| 配置引擎属主 = defaultTask | 正确(P1-3 已收敛),任何新代码不得跨任务触碰 |
| UI 只用 `lv_style_const_prop_t` | 正确且必要,禁止"顺手改成动态样式" |
| `ffsystem.c` 引用 `sys_clock.h` | 有意为之(中间件需要真实时间),保留但在分层图中显式标注 |
| UI 自绘而非 LVGL 控件 | 正确(1 bpp + 小 RAM),并是 Flash 裁剪的前提 |
---
**报告结束。** 建议把 §7 的 P0 六条直接追加进 `docs/项目审计改进清单.md`(沿用其编号体系,本报告编号 `N-01…N-18` 作为交叉引用),并把 §5/§6 的落地项写进 `docs/内存管理/RAM预算台账.md` 与 `docs/文档规范.md`。
+16 -2
View File
@@ -1,6 +1,6 @@
# 项目审计改进清单
- **审计日期**:2026-09-15
- **审计日期**:2026-09-15(**2026-09-18 补充**:《项目审查与优化建议报告》新增 P0-1,当日修复)
- **范围**:全部项目自有代码(App/、Drivers/BSP/、Src/ USER CODE、MDK-ARM/、test/、tools/、scripts/、.opencode/、docs/ 一致性);第三方库(Lib/、Middlewares/、CMSIS/HAL、lftpd 内部、LVGL)仅查项目侧误用
- **方法**:静态逐文件走读 + pre-commit 全库基线(cppcheck/规范/乱码,全绿)+ 三份 trap 文档交叉核对
- **严重度**:P0 = 确认功能 bug | P1 = 疑似 bug 或健壮性风险 | P2 = 改进 | P3 = 卫生
@@ -10,7 +10,7 @@
| 级别 | 数量 | 说明 |
|---|---|---|
| P0 | 0 | 未发现"当前必然失败"的核心流程 bug |
| P0 | 1 | ✅ P0-1 已修复(2026-09-18):配置引擎临时缓冲走 C 库 `malloc`,而 MicroLIB C 堆仅 512 B → 真机配置加载/保存必然失败(模拟器走宿主堆,双端表现分裂) |
| P1 | 10 | 6 项已修复(P1-2/3/4/5/6,2026-09-15;P1-9,2026-09-16);4 项未修:P1-1 ADC / P1-7 MAC 顺序 / P1-8 重建恢复 / P1-10 测试固件 RAM 溢出 |
| P2 | 16 | 健壮性/错误传播/文档失真 |
| P3 | 13 | 卫生项,可随其他修改顺手处理 |
@@ -22,6 +22,20 @@
---
## P0
### P0-1 配置引擎临时缓冲用 C 库 malloc(MicroLIB C 堆仅 512 B)→ 真机配置加载/保存必然失败 ✅ 已修复(2026-09-18)
- **位置**:`App/cfg_file.c` 修复前 `:485`(读缓冲 4,097B)/ `:522`(模板输出 5,120B)/ `:1038`(外科保存 Pass1 副本 ≤4,097B)/ `:1050`(保存输出 5,120B);根因参数在 `MDK-ARM/startup_stm32f407xx.s:43` `Heap_Size EQU 0x200`
- **证据**(静态三重,可独立复核):① `.map`(2026-09-16)`0x2001e878 - 0x00000200 Zero RW HEAP` 且 `__heap_limit = 0x2001ea78` → **C 库堆总量 = 0x200 = 512 B**;② 交叉引用 `cfg_file.o(i.s_load_file)` / `(i.s_write_template)` / `(i.cfg_file_save)` `refers to malloc.o(i.malloc)`,请求量 4,097 / 5,120 B,且全仓库无 `pvPortMalloc` retarget;③ `.lnp` 带 `--library_type=microlib`,MicroLIB 堆为固定区不可外扩。调用链:`Src/freertos.c:234 net_cfg_load()` → `Drivers/BSP/NETCFG/net_cfg.c:146 cfg_file_load()` → `s_load_file()` → `malloc`。
- **影响**:真机上配置文件**读不进也写不出**——上电加载回落工厂默认(192.168.1.100/24)、首启出厂模板生成失败、UI 保存与外科保存全部失败;日志签名 `read: malloc 4097B FAIL` / `template: malloc 5120B FAIL` / `load: file read FAIL, factory defaults kept`。**PC 模拟器走宿主 CRT 堆(MB 级)完全正常** → 双端行为分裂,双端 0 错 0 警也照不出来(`--cfgtest` PASS 亦不能证伪)。`test/verify_log.ps1` Forbidden 含 `FAIL`,一次 `@test` 即可捕获。
- **修复**(2026-09-18 实际实施):① `cfg_file_store` 后端新增 `cfg_store_alloc` / `cfg_store_free` / `cfg_store_heap_free` 三件套——MCU 端 = `pvPortMalloc`/`vPortFree`(取 FreeRTOS 堆 49,152B,与 `lftpd_io.c` 既有模式一致),模拟器端 = `malloc`/`free`;共享核心 `cfg_file.c` 内 C 库分配符号清零,双端仍可同源编译;② 新增编译期堆峰值预算 `CFG_HEAP_BUDGET = 10,240B`(typedef 负数组实现,ARMCC 5 C99 无 `_Static_assert`)——放大 `CFG_FILE_MAX_LEN`/`CFG_OUT_MAX` 会先编译失败,而不是退化为真机运行期失败;③ 四条失败路径(加载读失败 / 模板写失败 / 保存分配失败 / 保存写盘失败)补 `syslog_post(SYSLOG_SELFTEST, …)`,失败进自检记录页而非只留一行串口;`load: OK` 行附带 `heap free` 实测值;④ 文件头"属主任务"注释 netTask → defaultTask(P1-3 遗留漂移)。
- **未采用方案**:静态缓冲(4,097 + 5,120 = 9,217B,当时 SRAM1 仅余 4,488B 放不下,须先做 CCM 迁移);调大 MicroLIB `Heap_Size`/加 `--heap`(只是把"必然失败"变成"够大就侥幸",且该堆无碎片与水位观测)。**流式读写(免整文件驻留内存,+≈1.2KB 即可彻底摆脱暂态堆)列为后续改进项**,见《项目审查与优化建议报告》§4.4.1。
- **验证**:MCU `@build` 0 错 0 警;map 复核 `malloc.o` 不再参与链接、`Removing startup_stm32f407xx.o(HEAP), (512 bytes)` → 静态区 126,584 → **126,064 B**(余 4,488 → **5,008 B**),并新增 `cfg_file.o → cfg_file_store_mcu.o(cfg_store_alloc) → heap_4.o(pvPortMalloc)`;模拟器重建 0 警 + `lvgl_sim.exe --cfgtest` **PASS**(加载 523B → 外部编辑沉降重载 → 外科保存注释/值无损)。**待补**:真机 `@test` 一次,确认 `load: OK` 出现且 Forbidden `FAIL` 零命中。
- **附带发现(工具链陷阱)**:`App/cfg_file.c` 带 UTF-8 BOM(内含中文文档头字符串字面量);编辑工具剥掉 BOM 后 ARMCC 按 CP936 解码 → `s_doc_header` 报 4 条 `#870-D invalid multibyte character sequence`。已按 HEAD 的 BOM 状态复原。**改含中文字面量的文件后必须核对 BOM 未被剥**(`@build` 的 #870-D 是唯一暴露点)。
- **置信度**:high(失败机制 = 链接图 + 工具链堆尺寸双重静态确证;真机正向确认待 `@test`)。
---
## P1
### P1-1 ADC 采样数据路径产品构建中死亡