Files
STM32F4-Base/docs/审查记录/项目审查与优化建议报告.md
T

66 KiB
Raw Blame History

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/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 复现命令(本报告所有数字可重跑)

# 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/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 结构结论

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 的"文档-代码一致性门禁"合并实施。

✅ 2026-09-25 已处理:README 任务表已修正为 1024/1024/1024/2048/4096/512;两表均改为受管生成区(锚点 <!-- TASK_TABLE:BEGIN/END -->),scripts/gen_task_table.py 以 Src/freertos.c 的 osThreadAttr_t 为唯一事实源生成 / 校验(--check / --write / --diff),pre-commit task-table 钩子拦截漂移。改栈流程写入 docs/文档规范.md §4.2。

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)孤儿文档

内存管理/内存使用分析规划.md 无任何入链(项目自有文档中唯一孤儿)。建议:并入 RAM使用分析与优化方案.md 的"规划"章节,或在 docs/README.md 索引中显式列出。

✅ 2026-09-21 已处理:该文档与《STM32F4-Base 内存使用分析报告》一并并入 RAM使用分析与优化方案.md(现为内存管理主文档),两个文件已删除;口径与方法论见其 §8.4,快照数据见 docs/内存管理/data/。

D-5(P2)缺一个文档入口/索引

docs/ 根下平铺 12 篇 md + 8 个子目录,无 docs/README.md 索引;权威入口实际是 AGENTS.md(面向 agent)与 docs/架构设计/项目架构总览.md。新成员的阅读顺序不可见。

建议:新建 docs/README.md,按"上手 → 架构 → 模块 → 测试 → 规范 → 台账 → 外部资料"给出有序清单(约 60 行即可),并从 README.md 链接过去。

✅ 2026-09-25 已处理:已建 docs/README.md(目录结构树 + 推荐阅读顺序 + 按角色导航 + 9 大章节全量清单 + 新增文档约定 + 已知问题),并从根 README.md 链接过去。同轮把顶层 16 篇散落 md 归入 5 个新目录(架构设计//子系统设计//网络//审查记录//环境与部署/),全仓库 90 处引用同步。

D-6(P2)命名与目录约定不统一

中文目录名含空格(docs/UI设计/)在脚本/链接/CI 中易出错(本工程已出现多处需引号包裹的路径);同一目录混用 UI_界面制作流程.md(下划线+中文)与 内容页架构.md(纯中文)与 RemoDispBus协议与接收设计说明.md(中英连写)。

建议:确立 docs/文档规范.md:目录名一律无空格(UI设计 或 ui)、文件名"主题词+类型"、snake_case 用于英文段;新增文档必须出现在 docs/README.md。存量文档按 D-3 修链接时顺带重命名,一次做完。

✅ 2026-09-25 已处理:

  • 目录去空格:docs/UI 设计/ → docs/UI设计/,全仓库 67 处引用同步(含 markdown 链接的 URL 编码形态 UI%20设计);现全库已无含空格目录。
  • docs/文档规范.md 已创建:正式确立目录命名(无空格)、文件名风格(主题词+类型后缀 + 明确禁止字符)、编码换行、链接形态、门禁三档、结构要素、写作约定与常见错误速查。
  • 文件名风格统一:按 §1.2 约定复核 docs/ 全部文件(见规范 §1.2);内容页架构.md 等纯中文名保留(符合"主题词+类型"约定,未强制加下划线),仅纠正违反"无空格/无敏感字符"者。

D-7(P2)文档与代码之间没有一致性门禁

D-2/D-3 的共同根因:没有任何机械检查保证"文档说的 = 代码做的"。

建议:建立三档一致性检查(详见 §7 交付物):

检查 覆盖 手段
存在性 文档里的路径/文件/脚本必须存在 check_doc_links.py
数值 任务栈/优先级/端口/引脚表与代码一致 gen_task_table.py + 端口常量表提取
体积 文档中引用的"内存/Flash 数字"与最新 map 一致 复用 scripts/ram_report.py 输出

✅ 2026-09-25 / 09-26 已处理(三档全部落地):

  • 存在性:scripts/check_doc_links.py 已落地并接入 pre-commit doc-links 钩子。校验 md 互链(相对文档解析)与反引号内仓库根相对路径;豁免 [计划] 前缀、[示例] 标记、就近状态词、占位符、行内代码 span、外部 URL。全库 67 文档 0 失效。开发过程中实测抓出 7 处迁移漏改的图片相对链接(images/ 未改 ../images/)与 1 处真断链(tools/ui_mock/render_mockup.py,已标「已删除」)——门禁价值实证。
  • 数值:scripts/gen_task_table.py 以 Src/freertos.c 为唯一事实源生成/校验 README/AGENTS 任务表,pre-commit task-table 钩子拦截。
  • 体积(2026-09-26):scripts/check_mem_numbers.py 以 .map 为唯一事实源,校验 docs/内存管理/ 的受管快照区(MEM_SNAPSHOT 锚点)+ 扫描正文里的陈旧区域数字,pre-commit mem-numbers 钩子拦截。上线即抓出真实失真:文档写 RW_IRAM1 82,048 B / 62.6%、ROM 249,432 / 23.8%,实际 .map 已是 87,040 / 66.4%、262,660 / 25.0%(共修正 6 处)。
  • 文档体系规范(含门禁三档、受管区锚点、陈旧数字扫描的误报取舍)正式确立为 docs/文档规范.md。

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/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/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/host/verify_log.py(PowerShell 版 test/verify_log.ps1:34-35 已删除,2026-09-20 起为纯 Python)的 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/board/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 标准化目标:四层 + 一个闭环

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:

/* 屏幕与骨架 */
#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 行):

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/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/sync/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。