门禁扩容:新增 lv-conf-sync、simulator/ 纳入全部钩子;含 L3 定值/元件框架落地

本轮把"自研代码落在门禁之外"的口子收掉,同时落地 LVGL 特性裁剪与
L3 保护定值/元件框架。

门禁(.pre-commit-config.yaml,26 个钩子)
- 新增 scripts/check_lv_conf_sync.py + 钩子 lv-conf-sync:比对两份
  lv_conf.h 的**生效值**(未定义则取 lv_conf_internal.h 兜底默认),
  抓 MISMATCH / LVGL 不认的宏名 / 同文件重复定义
- 顶层 exclude 删掉 simulator/:其下 21 个 tracked 文件全是本项目自研
  代码,生成物(build/、SDL2/、tests/ref/、shot*)已在 .gitignore;
  doc-links 的 exclude 同步删除
- lv-conf-sync 的 files: 覆盖两端配置 → 只改 simulator/lv_conf.h 也触发
  (pre-commit 4.x 已移除 always: true,实测只告警并跳过,无兜底开关)
- 新增 prot-set-unit-test / prot-elm-unit-test 两个主机侧单测钩子
- S11(2.3.5) 判定细化:禁的是"用路径找文件",尖括号系统头带子目录
  (如 <sys/stat.h>)不再误报;全仓库复核仅 2 处项目代码判定变化

模拟器代码整改(纳入钩子后暴露,全部修代码而非豁免)
- cppcheck 4 处:sim_syslog.c 判空分支内解引用 p_epoch(真实 bug)、
  2 处 struct tm 未初始化、1 处指针可加 const
- coding-standard 61 处:44 处函数左大括号并到右括号同行、8 处补函数
  注释、3 处补文件头注释块、2 处 __ 前缀保护宏改 XXX_H、2 处 sprintf
  改 snprintf(写入字节数与原来完全一致)

LVGL 特性裁剪(F-1/F-2/F-3)
- 两端 lv_conf.h 对齐同一特性子集:删 26 个 LVGL 9.3 不存在的选项名、
  3 处重复定义,10 项靠默认值隐式生效的改为显式固化
- 实测对镜像体积中性;价值在防漂移,且模拟器 golden 回归跑的正是固件
  实际编译的配置

L3 软件侧
- App/algo/prot_set.* Q10 定值模型(范围校验 / 整表 all-or-nothing /
  校验和封存-自检-闭锁)+ prot_elm.* 通用"滞回+定时限"元件框架 7 实例
- test/host/prot_set、test/host/prot_elm 主机侧单测;uvprojx 挂载两处

文档同步
- pre-commit使用说明:§2.8 S11 口径、§2.15 lv-conf-sync 详解、新增 §5.5
  复盘(教训:exclude 只写真正不该进钩子的路径)
- AGENTS.md、UI_设计架构 §1.4 第 10 条、RAM 分析、审查报告刷新与销项

验证
- @build BUILD_OK 0/0(Code=194276 RO=15872 RW=876 ZI=139892)
- 模拟器重建:菜单 8 状态像素逐字节一致(池 5368 B)、11 内容页黄金参考
  全过(往返 5448 B)、remo sink 0/25600 位差异、--logtest/--cfgtest PASS
- 全仓库 cppcheck / coding-standard / no-mojibake --all-files 全过
- check_doc_links --all 129/0;check_mem_numbers --check 内存数字一致
- 端到端:往 simulator/lv_conf.h 注入一行漂移 → 钩子 FAIL(exit 1)
This commit is contained in:
2026-10-11 08:48:49 +08:00
parent 13a4e61c27
commit c936441c83
36 changed files with 2220 additions and 565 deletions
+52 -2
View File
@@ -2,11 +2,18 @@
# 详见 docs/代码规范/pre-commit使用说明.md
default_install_hook_types: [pre-commit, commit-msg]
# 顶层 exclude 只挡**第三方与生成物**。
# ⚠ 2026-10-11 修正:原先在这里整目录挡 `simulator/` 是错的——`simulator/` 下 21 个
# tracked 文件**全是本项目自研代码**(模拟器入口 main.c、平台后端 sim/*.c、回归脚本
# tests/*.py、两份配置与 README)。整目录豁免 = 自研代码落在所有钩子之外(连
# lv-conf-sync 都拦不到);实测把 simulator/ 纳入 cppcheck 后,立刻揪出
# sim_syslog.c 一处真实的空指针解引用。真正的生成物/第三方(`simulator/build/`、
# `simulator/SDL2/`、`simulator/tests/ref/`、`simulator/shot*.{bin,png}`、`__pycache__/`)
# **已在 .gitignore 里**,永远不会进 git 索引 → 无需在此重复豁免,故直接删掉该条。
exclude: |
(?x)^(
\.opencode/node_modules/|
Lib/lvgl/|
simulator/|
MDK-ARM/RTE/
)
@@ -153,11 +160,11 @@ repos:
language: system
files: \.md$
# 外部资料(芯片手册转录)链接形态不受控;第三方目录整体排除
# (`simulator/` 已不再排除:其下 3 个 README 是本项目文档,2026-10-11)
exclude: |
(?x)^(
\.opencode/node_modules/|
Lib/lvgl/|
simulator/|
MDK-ARM/RTE/|
docs/芯片手册/
)
@@ -177,6 +184,27 @@ repos:
files: ^(docs/内存管理/.*\.md|scripts/check_mem_numbers\.py|App/.*\.c|App/.*\.h|Src/.*\.c|Inc/.*\.h)$
pass_filenames: false
# 5d) 两份 lv_conf.h 特性子集一致性(2026-10-11 加,随 F-1/F-2/F-3 落地)
# 背景:`Lib/lv_conf.h`(MCU)与 `simulator/lv_conf.h`(PC)是两份独立文件,
# 此前长期漂移——模拟器早已 I1-only,MCU 端却让 11 个 `LV_DRAW_SW_SUPPORT_*`、
# 5 个未用控件类、`LV_USE_THEME_SIMPLE`、`LV_USE_FLEX/GRID` 靠 lv_conf_internal
# 默认值隐式生效 → 固件白链 **64,576 B 死代码**,且模拟器 golden 回归跑的是
# 另一套特性子集(等于验证不到固件实际编译的配置)。
# 口径:比对**生效值**(文件未定义则取 lv_conf_internal.h 的兜底默认),
# 并抓"写了 LVGL 不认的宏名"(`LV_USE_THEME_BASIC` 同类,实测两端共 26 处)
# 与同文件重复定义。规则正文见 docs/UI设计/UI_设计架构.md §1.4 第 10 条。
# ⚠ 触发范围(2026-10-11 两次修正):本钩子由**两端任一配置**或脚本自身触发。
# 原先顶层 exclude 整目录挡掉 `simulator/`,导致"只改 simulator/lv_conf.h"
# 的提交拦不到;已删该条(见文件顶部 exclude 注释),故这里可直接匹配
# `simulator/lv_conf.h`。(pre-commit 4.x 已移除 `always: true`,
# 实测对 local 钩子只告警并跳过——不能靠它兜底。)
- id: lv-conf-sync
name: 两份 lv_conf.h 特性子集一致性
entry: python scripts/check_lv_conf_sync.py
language: system
files: ^(Lib/lv_conf\.h|simulator/lv_conf\.h|scripts/check_lv_conf_sync\.py)$
pass_filenames: false
# 6) CubeMX 重生成存活审计(2026-09-29 加)
# 背景:.ioc 的 KeepUserCode 只保护 USER CODE 标记区;标记之外的手工改动
# 重生成即被覆盖,且多数不报编译错(崩溃采集被打回空 while(1)、加固宏被删)。
@@ -304,6 +332,28 @@ repos:
files: ^(App/algo/adc_health\.(c|h)|App/task/adc_task\.c|test/host/adc_health/.*|test/host/common/unit_test_harness\.py)$
pass_filenames: false
# 7i) 保护定值模型(P1 / L5-1 载体)主机侧单元测试(2026-10-11 加)
# 定值是保护动作的"输入宪法":范围校验、整表装载 all-or-nothing、
# 校验和封存-自检-闭锁(参考 tagPRS dwTagSum/dwTagSumN 模式)必须在
# 任何元件接线之前钉死——板端无法穷举越界定值注入,主机侧毫秒级全跑。
- id: prot-set-unit-test
name: 保护定值模型单测(主机侧)
entry: python test/host/prot_set/run_unit_test.py
language: system
files: ^(App/algo/prot_set\.(c|h)|test/host/prot_set/.*|test/host/common/unit_test_harness\.py)$
pass_filenames: false
# 7j) 保护元件框架(L3 P1 子集)主机侧单元测试(2026-10-11 加)
# 判据核(滞回/定时限/欠量方向/闭锁联动/时基回绕)是纯逻辑,
# 板端复现需要标准源 + 故障注入,主机侧用合成测量序列即可穷举
# 1.2×/0.9× 验收口径(规划 §8)。
- id: prot-elm-unit-test
name: 保护元件框架单测(主机侧)
entry: python test/host/prot_elm/run_unit_test.py
language: system
files: ^(App/algo/prot_elm\.(c|h)|App/algo/prot_set\.(c|h)|App/algo/phasor\.(c|h)|test/host/prot_elm/.*|test/host/common/unit_test_harness\.py)$
pass_filenames: false
# 8) 测试套件开关门禁(2026-10-01 加)
# 背景:test_config.h 的 TEST_SUITE_* 决定固件里编入哪个测试套件,
# 全部注释 = 产品构建。但此前**无任何门禁检查**——实测已提交的 HEAD 里
+5 -2
View File
@@ -30,7 +30,9 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
## 提交门禁(pre-commit)
- 任一失败即拦截,共 **18 个钩子**(`.pre-commit-config.yaml`,2026-10-10 清点):通用 5 个(`trailing-whitespace` / `end-of-file-fixer` / `check-added-large-files` / `check-merge-conflict` / `check-yaml`)+ `gitlint`(提交信息)+ `cppcheck`(warning/performance/portability/style)+ `test-cppcheck`(`test/` 只开 warning)+ `no-mojibake`(U+FFFD)+ `coding-standard`(规范 V1.0)+ 文档一致性三件套 `doc-links` / `task-table` / `mem-numbers` + `cubemx-survival`(重生成存活审计)+ 主机侧单测 `watchdog-unit-test` / `sys-clock-unit-test` / `cfg-file-unit-test` + `test-suite-switch`(测试开关=产品构建)。范围仅项目代码(第三方 `Lib/`/`Middlewares/`/CMSIS/HAL/`lftpd`/`dhara`/`MDK-ARM` 排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`)
- 任一失败即拦截,共 **26 个钩子**(`.pre-commit-config.yaml`,2026-10-11 清点):通用 5 个(`trailing-whitespace` / `end-of-file-fixer` / `check-added-large-files` / `check-merge-conflict` / `check-yaml`)+ `gitlint`(提交信息)+ `cppcheck`(warning/performance/portability/style)+ `test-cppcheck`(`test/` 只开 warning)+ `no-mojibake`(U+FFFD)+ `coding-standard`(规范 V1.0)+ 文档一致性三件套 `doc-links` / `task-table` / `mem-numbers` + `lv-conf-sync`(**两份 `lv_conf.h` 特性子集一致**,见 `docs/UI设计/UI_设计架构.md` §1.4 第 10 条)+ `cubemx-survival`(重生成存活审计)+ 主机侧单测 10 个 `watchdog` / `sys-clock` / `cfg-file` / `phasor` / `adc-input` / `vol-lock` / `time-sync` / `adc-health` / `prot-set` / `prot-elm`(均 `-unit-test` 后缀)+ `test-suite-switch`(测试开关=产品构建)。范围仅项目代码(第三方 `Lib/`/`Middlewares/`/CMSIS/HAL/`lftpd`/`dhara`/`MDK-ARM` 排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`)
- **`simulator/` 已纳入全部钩子**(2026-10-11 修正):顶层 `exclude` 原先整目录挡 `simulator/`,实际效果是把 **21 个本项目自研文件**(模拟器入口 `main.c`、`sim/*` 平台后端、`tests/*.py` 回归脚本、`lv_conf.h`/`dbg_log.h`、3 个 README)挡在门禁外——而那些生成物(`build/`、`SDL2/`、`tests/ref/`、`shot*.{bin,png}`、`__pycache__/`)**已在 `.gitignore` 里**,pre-commit 只看 git 索引,根本不需要重复豁免。纳入当天 `cppcheck` 即揪出 `sim/sim_syslog.c` 一处**真实空指针解引用**(`if (p_epoch == NULL || …) { *p_epoch = 0u; … }`),`coding-standard` 报 61 处(44 处函数左大括号 Allman 风格 / 8 处缺函数注释 / 3 处缺文件头 / 2 处 `__` 前缀保护宏 / 2 处 `sprintf` / 2 处 S11 误报)已全部处理;顺带细化 **S11 只禁"用路径找文件"**——`<sys/stat.h>` 这类尖括号系统头带子目录不再误报(全仓库复核:仅这 2 处项目代码判定变化)。⚠ **写模拟器代码与固件代码同标准**
- ⚠ **pre-commit 4.x 已移除 `always: true`**(实测对 `local` 钩子只告警 `Unexpected key(s)` 并跳过)→ 想要"总是跑"的钩子**必须让 `files:` 覆盖到所有会触发它的文件**,没有兜底开关。例:`lv-conf-sync` 的 `files:` 同时匹配 `Lib/lv_conf.h`、`simulator/lv_conf.h` 与脚本自身,两端任一改动都会跑
- **改写型钩子(`trailing-whitespace` + `end-of-file-fixer`)豁免 CubeMX/Keil 生成文件**(2026-10-09 定;配置里 YAML 锚点 `&cubemx-generated` 两处共用):`Src/*.{c,h}`、`Inc/*.{c,h}`、`Drivers/CMSIS/`、`Drivers/STM32F4xx_HAL_Driver/`、`Middlewares/`、`MDK-ARM/*.{uvprojx,uvoptx,sct,map}`、`.mxproject`、`*.ioc`——这类文件每次重生成按工具自己的风格写回(实测每次 10+ 个文件各被补 1 行末尾空行),改写只会制造提交抖动。⚠ **只读检查不受影响**:`cppcheck` / 乱码 / `cubemx-survival` / `task-table` / `mem-numbers` 仍覆盖 `Src/`、`Inc/`;`App/`、`Drivers/BSP/` 自研代码仍受改写型钩子检查
- 提交信息:**单行中文标题 ≤100 字符**(gitlint `title-max-length=100`)+ **正文每行 ≤80 字符**(gitlint 默认 `body-max-line-length`,本项目未关)→ 长说明一律用**短标题 + `git commit -F -` 多行正文**(中文长标题极易超限);UTF-8 无 BOM + `git commit -F <文件>`(防终端转码破坏)
- 代码规范 V1.0:小写下划线命名(`g_`/`s_`/`p_`/`a_` 前缀)、头文件保护宏 `XXX_H`、常量在右、文件头模块注释块 → `docs/代码规范/`
@@ -83,7 +85,7 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
- netTask:`net_stack_bring_up`(配置已由 defaultTask 在屏障前加载,本任务只读 RAM 副本 → `net_init` + PHY 强制 100M 全双工 → 递增网络代次号)→ 置 NET_READY → 循环 `net_poll` + `net_process_messages`;消费 `SYS_EVT_NET_RECONFIG` → 立即重建网络(新 IP 免重启生效,各 TCP 服务据代次号重建监听)
- defaultTask:`f_mount` forced(无卷自动 `f_mkfs` FAT32;挂载后卷健康探针:小写两击确认,FAT 区陈旧/擦除时自动重格式化自愈,`/config.ini` 回落工厂默认——2026-09-20 /log 写失败修复)→ 网络配置加载(`net_cfg_load`;旧档迁移已于 2026-10-10 删除,见「模块约定」网口配置条)→ STORAGE_READY → LED 闪烁;同时是 syslog(`/log/run.log`·`selftest.log`,定长 64B 记录)的**唯一写者**与**配置引擎属主**(`cfg_file` 静态态单任务收敛于此:UI 保存请求执行 `net_cfg_save_run` + 2s `cfg_file_poll`,经 `SYS_EVT_CFG_SAVE` 事件唤醒;netTask/uiTask 不得直接触碰配置引擎,见 P1-3)
- adcTask(**B1,2026-10-10;L1 层 v2.3;P1-5 事件驱动消费;v2.8 节拍源 TIM3 + STM32 侧 DMA,2026-10-10**):**纯消费者**——每拍调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 临界区原子快照(8 通道)+ **消费一帧 STM32 ADC1 DMA 双缓冲**(L1-3)→ 帧统计 + 每 10 s 心跳日志;取数一律经 `adc_input_raw()` **按语义**(L1-4,不写死下标);只取数、不做保护运算(V0.1 仅 TPAFE 三路 `UA`/`UN`/`IA` 测量有效,见 N-18)。**采样节拍(1 kHz)由 TIM3 更新事件硬件提供**(`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 TIM3 分支内调 `tpafe5160_start_conv_irq()`;同一更新事件经 **TRGO** 触发 ADC1 一轮 9 通道转换 → 两片转换器同源对齐);**消费唤醒与该节拍同源**——同一分支再调 `adc_tick_irq_notify()` 置位 `ADC_WAKE_FLAG`,任务用 `osThreadFlagsWait(..., osWaitForever)` 等唤醒(不再 1 ms 轮询;实测 `idle` 由 1~649 降到 **0**)。⚠ ISR 侧**只能**走 `adc_tick_irq_notify()`——它内含"消费者就绪"门(TIM3 在 `app_main_init` 就启动,比调度器早 ≈100 ms)。手写于 `Src/freertos.c` 的 `USER CODE` 区(同 timeSyncTask,**640 words** / **AboveNormal**——2026-10-11 L2 相量 + L1-6 有效性校验上链后静态最坏链 1080 B,512 words 余量跌破 1024 B 底线故抬栈),纳入 IWDG `required_mask`(`TASK_WD_ADC`)。详见 `docs/子系统设计/ADC采样节拍设计说明.md` §10、`App/algo/README.md`
- **采集与算法层(L1/L2)落在 `App/algo/`**(2026-10-10 建):现有 `adc_input.c/h` = L1(语义通道映射 + STM32 ADC1 DMA 消费)、`adc_health.c/h` = L1-6(采样有效性判定:静默/丢帧/单调性/错误增量,纯逻辑免桩单测)、`sample_ring.c/h` + `phasor.c/h` = L2(40 点采样环 + 全周傅氏 5 实例 + 双区交换,2026-10-11 台架闭环)。新算法模块一律进这里,勿塞进 `App/task/`(任务只做消费/编排);见 `App/algo/README.md`
- **采集与算法层(L1/L2/L3)落在 `App/algo/`**(2026-10-10 建):现有 `adc_input.c/h` = L1(语义通道映射 + STM32 ADC1 DMA 消费)、`adc_health.c/h` = L1-6(采样有效性判定:静默/丢帧/单调性/错误增量,纯逻辑免桩单测)、`sample_ring.c/h` + `phasor.c/h` = L2(40 点采样环 + 全周傅氏 5 实例 + 双区交换,2026-10-11 台架闭环)、`prot_set.c/h` + `prot_elm.c/h` = L3 软件侧(Q10 定值模型 + 校验和自检闭锁 + 通用"滞回+定时限"元件框架 7 实例,2026-10-11;**尚无任务调用 `prot_elm_run()`**——链接器移除、零足迹,板端接线随 protectTask 立项走 E-59 同款门禁)。新算法模块一律进这里,勿塞进 `App/task/`(任务只做消费/编排);见 `App/algo/README.md`
- 全景 `docs/架构设计/项目架构总览.md`(六层分层 / 启动时序 / 存储·网络·UI 栈 / 目录组织)
## 硬件引脚
@@ -130,6 +132,7 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
- `simulator/`(CMake + SDL2 预编译库)与 MCU 端共用 UI 源码(`ui_menu.c`、`ui_font_cn_12.c`),免烧录开发 UI:`& "simulator\build_sim.bat"`(按本机 VS 自动选生成器:16=2019 / 17=2022 / 18=2026,**勿把生成器版本写死**;`build/` 缓存生成器与本机 VS 不符时自动删除重建)→ 运行 `simulator\build\Debug\lvgl_sim.exe`
- 自检:`--cfgtest`(配置闭环)/ `--logtest`(日志引擎)/ `--standby shot.bin`(待机屏截图)/ `--idle <秒>`
- **模拟器代码与固件同标准受检**(2026-10-11):`simulator/` 下自研文件全部纳入 pre-commit(`cppcheck` / `coding-standard` / 乱码 / 改写型钩子 / `doc-links` / `lv-conf-sync`),生成物只靠 `.gitignore` 挡、**不在 `exclude` 里写目录名**(详见 `docs/代码规范/pre-commit使用说明.md` §5.5)。改 `simulator/lv_conf.h` 会触发 `lv-conf-sync`;改模拟器 C/H 会触发 cppcheck + 规范 V1.0
- UI 改动一律走 **`ui-verify` skill 五步门禁**;含新汉字的 UI 文案提交前先跑 **`font-regen` skill** 重生成中文字体
## 文档与图形
+58 -3
View File
@@ -15,10 +15,13 @@
| `adc_health.h` / `adc_health.c` | L1 | **采样有效性判定层**:吃"计数快照"(帧序号/错误计数),产出静默/丢帧/单调性/错误增量故障位——判定与告警编排分离 | `adc_health_init()` `adc_health_check()` `adc_health_flags()` | L1-6 |
| `sample_ring.h` / `sample_ring.c` | L2 输入 | **采样环缓冲**:按语义通道保存最近 2 个周波(40 点)采样值,供相量层"以当前时刻为终止"取窗 | `sample_ring_push_frame()` `sample_ring_window()` | L2 输入(为 L2-6 滑动窗供数) |
| `phasor.h` / `phasor.c` | L2 | **全周傅氏相量计算**:一周波 20 点单频 DFT → 基波/谐波相量(有效值口径),双区原子切换供无锁读 | `phasor_run()` `phasor_get()` `phasor_calc()` | L2-1 / L2-2 / L2-3 / L2-6 / L2-7 |
| `prot_set.h` / `prot_set.c` | L3 输入 | **保护定值模型**:Q10 定点 RAM 表(通讯地址对照参考 `g_tCommSetParaTab`)+ 范围校验 + 校验和封存自检闭锁 | `prot_set_init()` `prot_set_load_raw()` `prot_set_get_f()` `prot_set_selfcheck()` | §5 定值模型 / L5-1 |
| `prot_elm.h` / `prot_elm.c` | L3 | **保护元件框架**:通用"越限 + 返回系数滞回 + 定时限"核 + 数据驱动实例表(过流三段 / 过负荷 / 过压 / 欠压 / 零序过压) | `prot_elm_init()` `prot_elm_eval()` `prot_elm_run()` `prot_elm_active_mask()` | L3-1 / L3-3 / L3-4 / L3-6(软件侧) |
依赖关系(自下而上,无环):`adc_input.h`(纯枚举 + 声明)← `sample_ring` ← `phasor`;
`adc_health` **独立**(仅 `stdint.h`,不依赖本目录其它头)。
`sample_ring.c` / `phasor.c` / `adc_health.c` **零平台依赖**(只用 `math.h` / `stddef.h` / `stdint.h` 与本目录头);
`prot_set` **独立**(仅 `stdint.h`);`prot_elm` ← `prot_set` + `phasor`。
`sample_ring.c` / `phasor.c` / `adc_health.c` / `prot_set.c` / `prot_elm.c` **零平台依赖**
(只用 `math.h` / `stddef.h` / `stdint.h` 与本目录头);
`adc_input.c` 另依赖 `adc.h`(`hadc1`)与 `tpafe5160.h`(快照),主机侧以桩头替身。
## 2. 数据流(一拍 = TIM3 1 kHz 节拍)
@@ -148,6 +151,54 @@ flowchart TD
- 告警策略(在 `adc_task.c`,不在本层):严重事件/电平上升沿 `DBG_ERROR` +
`SYSLOG_SELFTEST`(每类每次开机一次,防刷屏写满 32KB selftest.log);下降沿 `DBG_INFO`。
### 3.5 `prot_set.c` / `prot_set.h` — 保护定值模型(P1 / L5-1 载体)
**解决的问题**:保护判据的"输入宪法"。参考系统把定值放 EEPROM + `tagPRS` 常量表
校验和自检;本层先做 **RAM 模型**(cfg_file 持久化属 P4),把编号 / 通讯地址 /
范围 / 默认值 / 校验和闭环钉死。
| API | 干什么 | 注意 |
|---|---|---|
| `prot_set_init()` | 装默认值 + 封存校验和(幂等) | 内核启动前可调 |
| `prot_set_load_raw(vals)` | 整表装载(P4 起 cfg_file 调用),逐项范围校验 | **all-or-nothing**:任一项越界整表不装载,`prot_set_bad_mask()` 指认越界项 |
| `prot_set_set(id, q10)` | 单项写入(范围校验) | **不自动重封存**——批量改完必须 `prot_set_seal()`,否则 selfcheck 判"非法改写" |
| `prot_set_get_f()` / `get_ctl()` | 工程量浮点 / 投退读取 | 单字读原子 |
| `prot_set_selfcheck()` | 重算 sum32 与封存正/反码比对,失配 → 闭锁 | L5-1;`prot_set_seal()` 是解锁的唯一合法路径 |
**要点**
- **Q10 定点**(×1024)与参考 `2^10` 折算口径一致;元数据表(地址/范围/默认)是
ROM 常量,校验和只覆盖 RAM 值区。
- **通讯地址逐项对照参考** `Comm/Pub/Const_CommSetPara.c` `g_tCommSetParaTab`
(基址 0x8820);单测 TC-PS-006 保证地址不重复、`min ≤ def ≤ max`、投退项 0..1。
- ⚠ **默认值/范围为拟定值**(参考 EEPROM 出厂表不在本仓库),待对照修订——
见规划 §5.4 落实进度注。
### 3.6 `prot_elm.c` / `prot_elm.h` — 保护元件框架(L3,P1 子集)
**解决的问题**:参考系统 89 个 `PRS_*.c` 各自实现"滞回 + 定时限"——本框架把这段
公共核收敛为**一个通用核 + 数据驱动实例表**(同 `phasor` 的 `k_inst` 模式),
新增元件 = 表加一行 + 定值加几项。
| API | 干什么 | 注意 |
|---|---|---|
| `prot_elm_init()` | 复位状态 + 解析相量实例索引(按名前缀 `C01/C02/C03`) | 须在 `phasor_init()` 之后调 |
| `prot_elm_eval(now_ms, meas)` | **纯判据**:定值 RAM + 测量数组 → 全元件状态机 | 主机单测入口;NULL 测量按 0 |
| `prot_elm_run(now_ms)` | 适配层:`phasor_get()` 组测量 → eval | 任一相量未就绪 → **整拍跳过**(不引入陈旧判据) |
| `prot_elm_state()` / `prot_elm_active_mask()` | 状态快照 / 动作位图 | 供 L4 出口与心跳日志 |
**要点**
- **滞回**:已启动用返回门槛(`定值 × 返回系数`),未启动用动作门槛——
过量/欠量方向由实例表 `over` 位统一处理(`UV` 是唯一欠量实例)。
- **定时限**:`(uint32_t)(int32_t)(now - t0) >= dly` 差值法,tick 回绕安全;
`dly=0` → 启动同拍即动作。返回**即时**(参考带 10 ms 返回延时,P4 接出口硬件时再补)。
- **闭锁联动**:`prot_set_locked()`(L5-1)或投退=0 → 该元件状态整体复位、不判据。
- **V0.1 实例**(N-18):`OC1/OC2/OC3/OL`(IA)、`OV/UV`(UA)、`3U0`(UN 外部通道)。
零序过流(IN 走 STM32)与过频/低频(待频率算法)**故意不在表中**。
- **尚未接线**:无任务调用 `prot_elm_run()`(链接器整对象移除,零足迹);
板端接线随 protectTask 立项(栈预算 + IWDG 心跳门禁,AGENTS E-59 同款流程)。
## 4. 关键约定
- **语义映射唯一权威口径**:`docs/硬件平台/ADC通道映射说明.md`。取数一律经 `adc_input_raw()`,
@@ -168,6 +219,8 @@ flowchart TD
| `phasor.c` `s_zone[2][5]` `phasor_t` | 160 B |
| `adc_input.c` `s_stm32_raw[16]` + `s_tpafe_raw[8]` | 48 B |
| `adc_health.c` 内部状态(9 × uint32 + uint8) | ≈ 40 B |
| `prot_set.c` `s_val[31]` + 封存/位图/闭锁 | 136 B(**未接线前被链接器移除,实测 0**) |
| `prot_elm.c` `s_rt[7]`(16 B/实例) | 112 B(同上,实测 0) |
| 运行期栈:`phasor_run()` 的 `win[20]` | 80 B/帧 |
改容量(`SAMPLE_RING_CYCLES` / 实例数 / `ADC_SEM_COUNT`)前后各跑一次
@@ -175,7 +228,7 @@ flowchart TD
## 6. 构建与测试
- **挂载**:四个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`,`../App/algo` 已在 `<IncludePath>` 内。
- **挂载**:七个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`,`../App/algo` 已在 `<IncludePath>` 内。
本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路)。
- **主机侧单测(L0.5)**——生产源文件以 `#include` 原样纳入,无需改产品代码:
@@ -184,6 +237,8 @@ flowchart TD
| 相量层(57 用例:系数表逐项复核 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 双区交换) | `& '.venv\Scripts\python.exe' test/host/phasor/run_unit_test.py` | `phasor-unit-test` |
| ADC 输入层(逐 Rank 注入互异码值钉死映射 + HAL 启动失败/超时分支) | `& '.venv\Scripts\python.exe' test/host/adc_input/run_unit_test.py` | `adc-input-unit-test` |
| 有效性校验(31 用例:100 ms 静默门限两侧 / 丢帧边沿 / 倒退锁存 / 错误增量 / tick 回绕 / NULL 守卫 / init 幂等) | `& '.venv\Scripts\python.exe' test/host/adc_health/run_unit_test.py` | `adc-health-unit-test` |
| 定值模型(30 用例:范围校验 / all-or-nothing 装载 / 封存-自检-闭锁 / Q10 往返 / 元数据表自检) | `& '.venv\Scripts\python.exe' test/host/prot_set/run_unit_test.py` | `prot-set-unit-test` |
| 元件框架(27 用例:滞回与定时限 / 欠量方向 / 投退 / 闭锁联动 / 时基回绕 / 位图聚合) | `& '.venv\Scripts\python.exe' test/host/prot_elm/run_unit_test.py` | `prot-elm-unit-test` |
改本目录任一 `.c/.h`,提交时对应钩子自动跑;详见 `docs/测试说明/相量层单元测试说明.md`。
- **板端**:`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍/心跳日志(`phasor(...)` 行、
+223
View File
@@ -0,0 +1,223 @@
/*
* 模块名称:保护元件框架(L3,P1 子集)——实现
* 模块功能:见 prot_elm.h。判据核 prot_elm_eval() 为纯逻辑(prot_set RAM +
* 测量数组 → 状态机),prot_elm_run() 是 phasor→测量的薄适配层。
*
* 创建日期:2026-10-11
*/
#include "prot_elm.h"
#include "prot_set.h"
#include "phasor.h"
#include <stddef.h>
/* 实例配置(ROM 常量表,数据驱动——新增元件只加一行,同 phasor k_inst 模式) */
typedef struct {
const char *name; /* 日志名 */
uint8_t over; /* 1 = 过量(越上限动作),0 = 欠量(越下限动作) */
prot_set_id_t en_id; /* 投退定值 */
prot_set_id_t set_id; /* 动作定值 */
prot_set_id_t dly_id; /* 延时定值(ms) */
prot_set_id_t ret_id; /* 返回系数定值(RET_OVER / RET_UNDER) */
const char *pprefix; /* 相量实例名前缀("C01"…),run() 适配层取数用 */
} pe_cfg_t;
static const pe_cfg_t k_pe[PE_COUNT] = {
{ "OC1-IA", 1u, PS_OC1_EN, PS_OC1_CUR, PS_OC1_DLY, PS_RET_OVER, "C01" },
{ "OC2-IA", 1u, PS_OC2_EN, PS_OC2_CUR, PS_OC2_DLY, PS_RET_OVER, "C01" },
{ "OC3-IA", 1u, PS_OC3_EN, PS_OC3_CUR, PS_OC3_DLY, PS_RET_OVER, "C01" },
{ "OL-IA", 1u, PS_OL_EN, PS_OL_CUR, PS_OL_DLY, PS_RET_OVER, "C01" },
{ "OV-UA", 1u, PS_OV_EN, PS_OV_VOL, PS_OV_DLY, PS_RET_OVER, "C02" },
{ "UV-UA", 0u, PS_UV_EN, PS_UV_VOL, PS_UV_DLY, PS_RET_UNDER, "C02" },
{ "3U0-UN", 1u, PS_3U0_EN, PS_3U0_VOL, PS_3U0_DLY, PS_RET_OVER, "C03" },
};
/* 运行时状态(picked/asserted 的对外快照 + 延时起点 + 相量实例索引缓存) */
typedef struct {
uint8_t enabled;
uint8_t picked;
uint8_t asserted;
uint32_t t0; /* picked 起始时基(picked=0 时无意义) */
uint8_t pidx; /* 相量实例索引;0xFF = 未解析/未找到 */
} pe_rt_t;
static pe_rt_t s_rt[PE_COUNT];
/*
* 函数功能:按名前缀在 phasor 实例表里找索引(开机解析一次)
* 入口参数:prefix - 相量实例名前缀(如 "C01")
* 返 回 值:实例索引;0xFF = 未找到(run() 对该元件整拍跳过)
*/
static uint8_t pe_find_phasor(const char *prefix) {
uint8_t i, n = phasor_inst_count();
for (i = 0u; i < n; i++) {
const phasor_inst_t *pi = phasor_inst(i);
uint8_t j = 0u, hit = 1u;
if (pi == 0) {
continue;
}
while ((prefix[j] != '\0') && (pi->name[j] != '\0')) {
if (prefix[j] != pi->name[j]) {
hit = 0u;
break;
}
j++;
}
if ((hit != 0u) && (prefix[j] == '\0')) {
return i;
}
}
return 0xFFu;
}
/*
* 函数功能:复位全部元件状态并解析相量索引(定值不动;幂等)
* 入口参数:无
* 返 回 值:无
* 限定条件:须在 phasor_init() 之后调用(否则实例表名查不到 → 全 0xFF)
*/
void prot_elm_init(void) {
uint32_t i;
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
s_rt[i].enabled = 0u;
s_rt[i].picked = 0u;
s_rt[i].asserted = 0u;
s_rt[i].t0 = 0u;
s_rt[i].pidx = pe_find_phasor(k_pe[i].pprefix);
}
}
/*
* 函数功能:纯判据步进——按当前定值 + 给定测量值推进所有元件
* 入口参数:now_ms - 单调时基(tick ms,差值法回绕安全)
* meas - PE_COUNT 个测量值(工程量 A/V);NULL 视为全 0
* 返 回 值:无
* 函数说明:投退=0 或定值闭锁(L5-1)→ 状态整体复位、不判据;
* 滞回 = 已启动用返回门槛(定值×系数)、未启动用动作门槛;
* 延时 dly=0 → 启动同拍即动作;返回即时(P4 接出口时再补返回延时)。
*/
void prot_elm_eval(uint32_t now_ms, const float meas[PE_COUNT]) {
uint8_t locked = prot_set_locked();
uint32_t i;
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
const pe_cfg_t *cfg = &k_pe[i];
uint8_t cond;
float m, set, ret;
s_rt[i].enabled = ((locked == 0u) && (prot_set_get_ctl(cfg->en_id) != 0u)) ? 1u : 0u;
if (s_rt[i].enabled == 0u) {
/* 退出/闭锁:状态整体复位(参考元件闭锁分支的 Rly/Timer 复位) */
s_rt[i].picked = 0u;
s_rt[i].asserted = 0u;
s_rt[i].t0 = 0u;
continue;
}
m = (meas != 0) ? meas[i] : 0.0f;
set = prot_set_get_f(cfg->set_id);
ret = set * prot_set_get_f(cfg->ret_id);
/* 滞回:已启动用返回门槛,未启动用动作门槛(参考 M_GRly_OverRelay) */
if (cfg->over != 0u) {
cond = (s_rt[i].picked != 0u) ? (uint8_t)(m > ret) : (uint8_t)(m > set);
} else {
cond = (s_rt[i].picked != 0u) ? (uint8_t)(m < ret) : (uint8_t)(m < set);
}
if (cond != 0u) {
float dly;
if (s_rt[i].picked == 0u) {
s_rt[i].picked = 1u;
s_rt[i].t0 = now_ms;
}
dly = prot_set_get_f(cfg->dly_id);
if (dly < 0.0f) {
dly = 0.0f;
}
/* 差值法比较,tick 回绕安全;dly=0 → 启动同拍即动作 */
if ((uint32_t)(int32_t)(now_ms - s_rt[i].t0) >= (uint32_t)dly) {
s_rt[i].asserted = 1u;
}
} else {
s_rt[i].picked = 0u;
s_rt[i].asserted = 0u;
s_rt[i].t0 = 0u;
}
}
}
/*
* 函数功能:适配层——从 phasor 使用区取 rms 组装测量数组后调判据核
* 入口参数:now_ms - 单调时基(tick ms)
* 返 回 值:无
* 限定条件:仅保护消费者任务上下文;工程量↔码值换算 = 1(L1-5 标定未实现)
* 函数说明:任一相量未就绪(未满一周波)→ **整拍跳过**,不引入陈旧/半新测量。
*/
void prot_elm_run(uint32_t now_ms) {
float meas[PE_COUNT];
phasor_t p;
uint32_t i;
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
if ((s_rt[i].pidx == 0xFFu) || (phasor_get(s_rt[i].pidx, &p) != 0)) {
return;
}
meas[i] = p.fAmSQ;
}
prot_elm_eval(now_ms, meas);
}
/*
* 函数功能:读单元件状态快照(拷贝语义)
* 入口参数:id - 元件编号;out - 快照输出(NULL 直接返回)
* 返 回 值:无;越界编号输出全零
*/
void prot_elm_state(prot_elm_id_t id, prot_elm_state_t *out) {
if (out == 0) {
return;
}
out->enabled = 0u;
out->picked = 0u;
out->asserted = 0u;
out->pick_ms = 0u;
if (id < PE_COUNT) {
out->enabled = s_rt[(uint32_t)id].enabled;
out->picked = s_rt[(uint32_t)id].picked;
out->asserted = s_rt[(uint32_t)id].asserted;
out->pick_ms = s_rt[(uint32_t)id].t0;
}
}
/*
* 函数功能:聚合动作位图
* 入口参数:无
* 返 回 值:bit i = 元件 i asserted(供 L4 出口 / 心跳日志)
*/
uint32_t prot_elm_active_mask(void) {
uint32_t mask = 0u;
uint32_t i;
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
if (s_rt[i].asserted != 0u) {
mask |= (1uL << i);
}
}
return mask;
}
/*
* 函数功能:取实例名(日志用)
* 入口参数:id - 元件编号
* 返 回 值:ROM 常量名;越界编号返回 "PE??"
*/
const char *prot_elm_name(prot_elm_id_t id) {
if (id >= PE_COUNT) {
return "PE??";
}
return k_pe[(uint32_t)id].name;
}
+84
View File
@@ -0,0 +1,84 @@
/*
* 模块名称:保护元件框架(L3,P1 子集)
* 模块功能:通用"越限 + 返回系数滞回 + 定时限"元件核 + 实例表(数据驱动,
* 同 phasor 实例表模式)。判据为**纯逻辑**(吃测量值 + 定值),
* 主机侧可离线单测;出口/SOE(L4)与任务编排为后续。
*
* V0.1 有效实例(N-18:仅 TPAFE 三路):
* OC1/OC2/OC3 三段式相间过流(A 相 IA,L3-1)
* OL 过负荷(A 相 IA,L3-3)
* OV/UV 过压/欠压(UA,L3-4)
* 3U0 零序过压(UN 外部零序电压通道,L3-6)
* ⚠ 过频/低频(L3-5)随频率测量算法(P2)落地,本表暂不含。
*
* 与参考系统的口径差异(有意简化,均已在规划文档登记):
* - 返回门槛 = 定值 × 返回系数(参考经 CN_SET_RET 独立定值,等价);
* - 返回**即时**(参考 GRly_TimeRelay 带 10 ms 返回延时——V0.1 无出口硬件,
* 抖动只影响日志,P4 接出口时再补返回延时);
* - 复压/方向/无压无流闭锁(dwSetFyCtrl 类)随 P2/P3 元件扩展;
* - 工程量↔码值换算系数暂为 1(L1-5 标定未实现,同 phasor 层口径)。
*
* 线程约定:init 内核启动前;eval/run 仅保护消费者任务上下文(P1 阶段 = 单测;
* 板端接线随 protectTask 立项,走栈预算 + IWDG 心跳门禁)。
*
* 创建日期:2026-10-11
*/
#ifndef PROT_ELM_H
#define PROT_ELM_H
#include <stdint.h>
/* 元件实例编号(枚举序 = 实例表序 = 测量数组序) */
typedef enum {
PE_OC1 = 0, /* 过流 Ⅰ 段(IA) */
PE_OC2, /* 过流 Ⅱ 段(IA) */
PE_OC3, /* 过流 Ⅲ 段(IA) */
PE_OL, /* 过负荷(IA) */
PE_OV, /* 过压(UA) */
PE_UV, /* 欠压(UA,欠量方向) */
PE_3U0, /* 零序过压(UN) */
PE_COUNT
} prot_elm_id_t;
/* 单元件状态快照(读侧拷贝语义,见 prot_elm_state) */
typedef struct {
uint8_t enabled; /* 本拍投入(定值投退 && 未闭锁) */
uint8_t picked; /* 越限启动(滞回后电平) */
uint8_t asserted; /* 定时限到点(动作) */
uint32_t pick_ms; /* 本次启动起始时基(0 = 未启动过;asserted 延时观测用) */
} prot_elm_state_t;
/*
* 函数功能:复位全部元件状态(定值不动;幂等,内核启动前可调)
*/
void prot_elm_init(void);
/*
* 函数功能:纯判据步进——按当前定值(prot_set RAM)+ 给定测量值推进所有元件
* 入口参数:now_ms - 单调时基(tick ms,回绕安全:差值法)
* meas - PE_COUNT 个测量值(工程量:A / V),NULL 视为全 0
* 返 回 值:无
* 限定条件:仅保护消费者任务上下文;调用方保证定值已装载
* 函数说明:越限方向由实例表决定;投退=0 或定值闭锁(L5-1)→ 状态复位、
* 不判据。延时判定用 (uint32_t)(int32_t)(now - t0) >= dly,回绕安全。
*/
void prot_elm_eval(uint32_t now_ms, const float meas[PE_COUNT]);
/*
* 函数功能:适配层——从 phasor 使用区取 rms 组装测量数组后调 prot_elm_eval
* 入口参数:now_ms - 同上
* 说明:phasor 未就绪(-2,未满一周波)的实例本拍按"测量=上一拍值"处理会
* 引入陈旧判据 → 直接**整拍跳过**(不 eval),与 adc_task 跳拍同口径。
*/
void prot_elm_run(uint32_t now_ms);
/* 状态快照拷贝(NULL 安全;越界全零) */
void prot_elm_state(prot_elm_id_t id, prot_elm_state_t *out);
/* 动作位图(bit i = 元件 i asserted),供 L4 出口 / 心跳日志 */
uint32_t prot_elm_active_mask(void);
/* 实例名(ROM 常量,日志用) */
const char *prot_elm_name(prot_elm_id_t id);
#endif /* PROT_ELM_H */
+228
View File
@@ -0,0 +1,228 @@
/*
* 模块名称:保护定值模型(P1,L5-1 载体)——实现
* 模块功能:见 prot_set.h。纯逻辑,零平台依赖(仅 stdint.h + 自身头)。
*
* ⚠ 默认值/范围为拟定值(参考 EEPROM 出厂默认表不在本仓库,规划 §10.3 待补);
* 通讯地址逐项对照参考 Comm/Pub/Const_CommSetPara.c g_tCommSetParaTab。
*
* 创建日期:2026-10-11
*/
#include "prot_set.h"
/* 元数据表:顺序必须与 prot_set_id_t 严格一致(逐行带地址注释,同 phasor
* k_inst 的朴素行式风格) */
const prot_set_meta_t g_prot_set_meta[PS_ID_COUNT] = {
/* addr ctl default min max 说明 */
{ 0x8856, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* OC1 投退 */
{ 0x8857, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* OC1 跳闸投退 */
{ 0x8858, 0u, PROT_SET_Q10(10.0), 0, PROT_SET_Q10(100.0) }, /* OC1 电流 A */
{ 0x8859, 0u, PROT_SET_Q10(0.0), 0, PROT_SET_Q10(60000) }, /* OC1 延时 ms */
{ 0x885A, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) },
{ 0x885B, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) },
{ 0x885C, 0u, PROT_SET_Q10(5.0), 0, PROT_SET_Q10(100.0) },
{ 0x885D, 0u, PROT_SET_Q10(500.0), 0, PROT_SET_Q10(60000) },
{ 0x885E, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) },
{ 0x885F, 1u, PROT_SET_Q10(0), 0, PROT_SET_Q10(1) }, /* OC3 默认告警 */
{ 0x8860, 0u, PROT_SET_Q10(2.0), 0, PROT_SET_Q10(100.0) },
{ 0x8861, 0u, PROT_SET_Q10(3000.0), 0, PROT_SET_Q10(60000) },
{ 0x8871, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* 过负荷投退 */
{ 0x8872, 1u, PROT_SET_Q10(0), 0, PROT_SET_Q10(1) }, /* 默认告警 */
{ 0x8873, 0u, PROT_SET_Q10(1.0), 0, PROT_SET_Q10(100.0) },
{ 0x8874, 0u, PROT_SET_Q10(10000.0),0, PROT_SET_Q10(60000) },
{ 0x0000, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* OV 投退(本地) */
{ 0x8029, 0u, PROT_SET_Q10(120.0), 0, PROT_SET_Q10(200.0) }, /* 过压 V */
{ 0x802A, 0u, PROT_SET_Q10(10000.0),0, PROT_SET_Q10(60000) },
{ 0x0000, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* UV 投退(本地) */
{ 0x8027, 0u, PROT_SET_Q10(20.0), 0, PROT_SET_Q10(200.0) }, /* 欠压 V */
{ 0x8028, 0u, PROT_SET_Q10(10000.0),0, PROT_SET_Q10(60000) },
{ 0x888E, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* 3U0 投退 */
{ 0x888F, 0u, PROT_SET_Q10(15.0), 0, PROT_SET_Q10(200.0) }, /* 3U0 V */
{ 0x8890, 0u, PROT_SET_Q10(5000.0), 0, PROT_SET_Q10(60000) },
{ 0x0000, 0u, PROT_SET_Q10(0.95), PROT_SET_Q10(0.5), PROT_SET_Q10(1.0) }, /* 过量返回系数 */
{ 0x0000, 0u, PROT_SET_Q10(1.05), PROT_SET_Q10(1.0), PROT_SET_Q10(1.5) }, /* 欠量返回系数 */
{ 0x8820, 0u, PROT_SET_Q10(10000.0),PROT_SET_Q10(100.0), PROT_SET_Q10(500000.0) },
{ 0x8821, 0u, PROT_SET_Q10(100.0), PROT_SET_Q10(1.0), PROT_SET_Q10(200.0) },
{ 0x8825, 0u, PROT_SET_Q10(100.0), PROT_SET_Q10(1.0), PROT_SET_Q10(10000.0) },
{ 0x8826, 0u, PROT_SET_Q10(5.0), PROT_SET_Q10(1.0), PROT_SET_Q10(10.0) },
};
static uint32_t s_val[PS_ID_COUNT]; /* RAM 值区(uint32 承载 int32,校验和按字覆盖) */
static uint32_t s_sum; /* 封存的正码 */
static uint32_t s_sumn; /* 封存的反码 */
static uint32_t s_bad_mask; /* load_raw 越界位图 */
static uint8_t s_locked; /* 定值非法 → 闭锁(L5-1) */
/*
* 函数功能:sum32 覆盖 RAM 值区(参考 Fun_CalSum32 同款:32 位字累加自然回绕)
* 入口参数:无
* 返 回 值:校验和正码
*/
static uint32_t prot_sum32(void) {
uint32_t sum = 0u;
uint32_t i;
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
sum += s_val[i];
}
return sum;
}
/*
* 函数功能:把元数据表默认值刷进 RAM 值区
* 入口参数:无
* 返 回 值:无
*/
static void prot_load_defaults(void) {
uint32_t i;
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
s_val[i] = (uint32_t)g_prot_set_meta[i].def;
}
}
/*
* 函数功能:装载出厂默认值并封存校验和(幂等)
* 入口参数:无
* 返 回 值:无
*/
void prot_set_init(void) {
prot_load_defaults();
s_bad_mask = 0u;
s_locked = 0u;
prot_set_seal();
}
/*
* 函数功能:取最近一次 load_raw 的越界位图
* 入口参数:无
* 返 回 值:bit i = 第 i 项越界(load_raw 失败后有效)
*/
uint32_t prot_set_bad_mask(void) {
return s_bad_mask;
}
/*
* 函数功能:整表装载外部值(P4 起由 cfg_file 定值段调用),逐项范围校验
* 入口参数:vals - PS_ID_COUNT 个 Q10 值
* 返 回 值:0 全部合法(已装载并封存)/ -1 存在越界项或空指针(整表不装载)
* 函数说明:all-or-nothing——宁可全旧不可半新(定值一致性优先);
* 越界项经 prot_set_bad_mask() 指认。
*/
int prot_set_load_raw(const int32_t *vals) {
uint32_t mask = 0u;
uint32_t i;
if (vals == 0) {
return -1;
}
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
if ((vals[i] < g_prot_set_meta[i].min) || (vals[i] > g_prot_set_meta[i].max)) {
mask |= (1uL << i);
}
}
s_bad_mask = mask;
if (mask != 0u) {
return -1;
}
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
s_val[i] = (uint32_t)vals[i];
}
prot_set_seal();
return 0;
}
/*
* 函数功能:单项写入(范围校验)
* 入口参数:id - 定值项编号;q10 - Q10 定点值
* 返 回 值:0 / -1 越界或非法编号拒写(原值不动)
* 函数说明:**不自动重封校验和**——批量改完必须显式 prot_set_seal(),
* 否则 selfcheck 判"定值被非法改写"(参考系统改定值走重写整表)。
*/
int prot_set_set(prot_set_id_t id, int32_t q10) {
if (id >= PS_ID_COUNT) {
return -1;
}
if ((q10 < g_prot_set_meta[id].min) || (q10 > g_prot_set_meta[id].max)) {
return -1;
}
s_val[(uint32_t)id] = (uint32_t)q10;
return 0;
}
/*
* 函数功能:读定值 Q10 原值
* 入口参数:id - 定值项编号
* 返 回 值:Q10 值;非法编号返回 0
*/
int32_t prot_set_get(prot_set_id_t id) {
if (id >= PS_ID_COUNT) {
return 0;
}
return (int32_t)s_val[(uint32_t)id];
}
/*
* 函数功能:读定值工程量浮点值
* 入口参数:id - 定值项编号
* 返 回 值:Q10 → float;非法编号返回 0.0
*/
float prot_set_get_f(prot_set_id_t id) {
return PROT_SET_FROM_Q10(prot_set_get(id));
}
/*
* 函数功能:读投退/枚举项
* 入口参数:id - 定值项编号
* 返 回 值:!=0 视为投入(1/0)
*/
uint8_t prot_set_get_ctl(prot_set_id_t id) {
return (prot_set_get(id) != 0) ? 1u : 0u;
}
/*
* 函数功能:实时计算校验和(不读封存值)
* 入口参数:无
* 返 回 值:sum32 正码
*/
uint32_t prot_set_checksum(void) {
return prot_sum32();
}
/*
* 函数功能:封存当前值区的正/反码对(合法改定值闭环的最后一步)
* 入口参数:无
* 返 回 值:无
* 函数说明:封存同时解除闭锁——selfcheck 失配后的唯一合法解锁路径。
*/
void prot_set_seal(void) {
s_sum = prot_sum32();
s_sumn = ~s_sum;
s_locked = 0u;
}
/*
* 函数功能:定值自检(L5-1)——重算 sum32 并与封存值比对
* 入口参数:无
* 返 回 值:0 通过 / -1 失配(已置闭锁)
* 函数说明:失配 → s_locked 置位并保持,直到下一次 seal;
* 元件层每拍读 prot_set_locked() 决定整体复位(联动闭锁)。
*/
int prot_set_selfcheck(void) {
if (prot_sum32() != s_sum) {
s_locked = 1u; /* 值区与封存不符:定值被非法改写 → 闭锁 */
}
if ((s_sum != (~s_sumn)) || (s_locked != 0u)) {
s_locked = 1u;
return -1;
}
return 0;
}
/*
* 函数功能:读闭锁状态
* 入口参数:无
* 返 回 值:1 = 定值非法(保护应整体闭锁)/ 0 = 正常
*/
uint8_t prot_set_locked(void) {
return s_locked;
}
+116
View File
@@ -0,0 +1,116 @@
/*
* 模块名称:保护定值模型(P1,L5-1 载体)
* 模块功能:保护定值的 RAM 模型——编号 / 通讯地址 / 范围 / 默认值 / 校验和自检。
* 纯逻辑(仅 stdint.h),主机侧可离线单测;持久化(cfg_file 定值段)
* 属 P4,本层只提供 load_raw/validate 的装载接口。
*
* 设计口径(docs/算法/继电保护功能复现规划.md §5):
* - 定点存储:Q10(×1024),与参考系统定点系数 2^10 一致(§5.3);
* - 通讯地址:与参考 g_tCommSetParaTab(Comm/Pub/Const_CommSetPara.c)逐项对齐,
* 基址 0x8820(=34848);告警类沿用运行参数段 0x8027/0x8029;
* - 校验和(L5-1):sum32 正码 + 反码成对封存(参考 tagPRS 的
* dwTagSum/dwTagSumN 模式),selfcheck 失配 → 置闭锁;
* - ⚠ 默认值/范围为**拟定值**:参考 EEPROM 出厂默认表未在本仓库,
* 待对照后逐项修订(规划 §10.3 待补项);范围先按 DTU 馈线典型量级。
*
* 线程约定:装载(init/load_raw)在 defaultTask(P4 起);读在保护消费者任务。
* 读接口返回快照值(单个 int32 读取原子);写与封存必须同一任务(P4 定)。
*
* 创建日期:2026-10-11
*/
#ifndef PROT_SET_H
#define PROT_SET_H
#include <stdint.h>
/* Q10 定点:工程量 × 1024(与参考 2^10 口径一致) */
#define PROT_SET_Q10(v) ((int32_t)(((double)(v)) * 1024.0))
#define PROT_SET_FROM_Q10(v) ((float)((double)(v) / 1024.0))
/* 定值项编号(枚举序 = RAM 数组序 = 校验和覆盖序,只增不删不改序) */
typedef enum {
/* --- 三段式相间过流(A 相,L3-1):投退/跳闸投退/电流/延时 --- */
PS_OC1_EN = 0, /* 0x8856 Ⅰ段投退 */
PS_OC1_TRIP, /* 0x8857 Ⅰ段跳闸投退(0 告警 / 1 跳闸) */
PS_OC1_CUR, /* 0x8858 Ⅰ段电流定值(A,二次值) */
PS_OC1_DLY, /* 0x8859 Ⅰ段延时(ms) */
PS_OC2_EN, /* 0x885A */
PS_OC2_TRIP, /* 0x885B */
PS_OC2_CUR, /* 0x885C */
PS_OC2_DLY, /* 0x885D */
PS_OC3_EN, /* 0x885E */
PS_OC3_TRIP, /* 0x885F */
PS_OC3_CUR, /* 0x8860 */
PS_OC3_DLY, /* 0x8861 */
/* --- 过负荷(A 相,L3-3) --- */
PS_OL_EN, /* 0x8871 */
PS_OL_TRIP, /* 0x8872 */
PS_OL_CUR, /* 0x8873 */
PS_OL_DLY, /* 0x8874 */
/* --- 过压 / 欠压(UA,L3-4;沿用参考告警段 0x8029/0x8027) --- */
PS_OV_EN, /* 本地投退(参考告警类无独立投退,V0.1 补) */
PS_OV_VOL, /* 0x8029 过电压门限(V,二次值) */
PS_OV_DLY, /* 0x802A 过电压延时(ms) */
PS_UV_EN, /* 本地投退 */
PS_UV_VOL, /* 0x8027 低电压门限(V,二次值) */
PS_UV_DLY, /* 0x8028 低电压延时(ms) */
/* --- 零序过压(UN,L3-6) --- */
PS_3U0_EN, /* 0x888E */
PS_3U0_VOL, /* 0x888F 3U0 定值(V,二次值) */
PS_3U0_DLY, /* 0x8890 延时(ms) */
/* --- 公共 --- */
PS_RET_OVER, /* 过量返回系数(返回门槛 = 定值 × 系数,<1) */
PS_RET_UNDER, /* 欠量返回系数(返回门槛 = 定值 × 系数,>1) */
PS_PT_PRI, /* 0x8820 PT 一次额定(V) */
PS_PT_SEC, /* 0x8821 PT 二次额定(V) */
PS_CT_PRI, /* 0x8825 CT 一次额定(A) */
PS_CT_SEC, /* 0x8826 CT 二次额定(A) */
PS_ID_COUNT
} prot_set_id_t;
/* 定值项元数据(ROM 常量表,校验和自检不覆盖它——只覆盖 RAM 值区) */
typedef struct {
uint16_t comm_addr; /* 通讯信息体地址(0 = 本地项,不上送) */
uint8_t is_ctl; /* 1 = 投退/枚举项(0/1),0 = 模拟量项 */
int32_t def; /* 默认值(Q10;投退项为 0/1 的 Q10) */
int32_t min; /* 下限(Q10) */
int32_t max; /* 上限(Q10) */
} prot_set_meta_t;
extern const prot_set_meta_t g_prot_set_meta[PS_ID_COUNT];
/*
* 函数功能:装载出厂默认值并封存校验和(幂等,内核启动前可调)
*/
void prot_set_init(void);
/*
* 函数功能:整表装载外部值(P4 起由 cfg_file 定值段调用),逐项范围校验
* 入口参数:vals - PS_ID_COUNT 个 Q10 值
* 返 回 值:0 全部合法(已装载并封存)/ -1 存在越界项(**整表不装载**,
* 返回前可经 prot_set_bad_mask() 取越界位图)
*/
int prot_set_load_raw(const int32_t *vals);
/* 越界位图(bit i = 第 i 项越界;load_raw 失败后有效) */
uint32_t prot_set_bad_mask(void);
/*
* 函数功能:单项写入(范围校验;**不自动重封校验和**——批量改完必须
* 显式 prot_set_seal(),否则 selfcheck 判"定值被非法改写")
* 返 回 值:0 / -1 越界拒写
*/
int prot_set_set(prot_set_id_t id, int32_t q10);
int32_t prot_set_get(prot_set_id_t id); /* Q10 原值 */
float prot_set_get_f(prot_set_id_t id); /* 工程量浮点 */
uint8_t prot_set_get_ctl(prot_set_id_t id); /* 投退项:!=0 视为投入 */
/* 校验和(L5-1):sum32 覆盖 RAM 值区;seal 封存正/反码对 */
uint32_t prot_set_checksum(void);
void prot_set_seal(void);
/* 自检:重算并与封存值比对,失配 → 置闭锁并返回 -1(已闭锁也返回 -1) */
int prot_set_selfcheck(void);
uint8_t prot_set_locked(void);
#endif /* PROT_SET_H */
+85 -27
View File
@@ -7,22 +7,40 @@
#ifndef LV_CONF_H
#define LV_CONF_H
/* ⚠ 本文件与 simulator/lv_conf.h 必须保持**同一特性子集**,已由门禁校验:
* `python scripts/check_lv_conf_sync.py`(pre-commit 钩子 lv-conf-sync,always 跑)。
* 2026-10-11 清理:删掉 14 个 LVGL 9.3 **不存在**的选项(LVGL 8 / 9.4 模板残留,
* 写了不生效,与已删的 LV_USE_THEME_BASIC 同类):LV_USE_PNG / JPG / ANIMATION /
* ENCODER / MOUSEWHEEL / IMGBTN / METER / COLORWHEEL / TEXTLINE / ASSERT_USER_CODE /
* ASSERT_LAYOUT、LV_DRAW_SW_YIELD / ASYNC、LV_COLOR_CHROMA_KEY。
* 另把此前靠 lv_conf_internal 默认值隐式生效的开关显式固化为 0(见下"核心特性"块)。 */
/* 1bpp 单色 */
#define LV_COLOR_DEPTH 1
#define LV_COLOR_CHROMA_KEY 0x000000
/* 必需控件(菜单依赖 flex/label/button) */
#define LV_USE_OBJ 1
#define LV_USE_FLEX 1
#define LV_USE_LABEL 1
#define LV_USE_BUTTON 1
/* 控件裁剪(F-2/F-3,2026-10-11):本项目 UI 全部自绘——实测 Drivers/BSP/UI、App、
* simulator 三处**零控件构造函数、零 flex/grid 布局调用**,只用 lv_obj_create +
* lv_theme_mono_init。此前这些类靠 lv_theme_simple(9.3 默认 =1)当锚点链进镜像:
* label 4,042 + buttonmatrix 3,586 + textarea 3,632 + bar 1,800 + button 26
* = 13,086 B;另 flex 2,776 + grid 2,592 = 5,368 B。
* ⚠ 将来引入控件型页面(U2 原型库)时按需逐项打开,并同步 simulator/lv_conf.h
* 后复跑 ui-verify 五步门禁。
* 注:原 LV_USE_OBJ 在 9.3 不是有效选项(lv_conf_internal 无此宏),已删。 */
#define LV_USE_FLEX 0 /* F-3:全仓无 lv_obj_set_flex_* 调用 */
#define LV_USE_GRID 0 /* F-3:全仓无 lv_obj_set_grid_* 调用 */
#define LV_USE_LABEL 0
#define LV_USE_BUTTON 0
#define LV_USE_BUTTONMATRIX 0
#define LV_USE_BAR 0
#define LV_USE_TEXTAREA 0
#define LV_USE_MENU 0
#define LV_USE_IMAGEBUTTON 0 /* 9.3 默认 1;显式关死,与 simulator 一致 */
/* 刷新周期(ms):离屏渲染,无需高刷 */
#define LV_DEF_REFR_PERIOD 20
/* 关闭动画/日志,省 RAM 与代码 */
#define LV_USE_ANIMATION 0
/* 关闭日志,省 RAM 与代码。
* 注:9.3 **没有** LV_USE_ANIMATION(动画属核心),原该行是无效项,已删。 */
#define LV_USE_LOG 0
/* 不使用 LVGL 自带 OS 抽象,由 ui_task 手动调用 lv_timer_handler */
@@ -34,13 +52,12 @@
#define LV_MEM_SIZE (40 * 1024)
#define LV_MEM_ADR 0 /* 0 = 内部静态分配 */
/* 断言全部关闭,减少体积 */
/* 断言全部关闭,减少体积(9.3 只有这 4 个 ASSERT 宏;原 ASSERT_USER_CODE /
* ASSERT_LAYOUT 是 LVGL 8 残留,已删) */
#define LV_USE_ASSERT_NULL 0
#define LV_USE_ASSERT_MALLOC 0
#define LV_USE_ASSERT_USER_CODE 0
#define LV_USE_ASSERT_OBJ 0
#define LV_USE_ASSERT_STYLE 0
#define LV_USE_ASSERT_LAYOUT 0
/* 内置字体:先启用 Latin 以通过编译;
* 中文 1bpp 字库由 fontconverter 生成后通过 ui_font_cn_12 接入并替换默认字体 */
@@ -66,27 +83,50 @@
/* 文本编码 UTF-8(菜单含中文) */
#define LV_TXT_ENC LV_TXT_ENC_UTF8
/* 主题:关闭默认主题,控件用内置基础样式,省代码 */
/* 主题:只用 mono(ui_port.c 显式 lv_theme_mono_init(disp, true, ...) + set_theme + apply)。
* ⚠ 9.3 **没有** LV_USE_THEME_BASIC —— 原那条是无效配置(写了也不生效,已删)。
* ⚠ 关键:LV_USE_THEME_SIMPLE 默认 =1,lv_display_create() 会因此强链 lv_theme_simple,
* 而它正是把上面那些控件类拉进镜像的锚点(主题本体 866 B + 锚定控件 13,086 B)。 */
#define LV_USE_THEME_DEFAULT 0
#define LV_USE_THEME_BASIC 0
#define LV_USE_THEME_SIMPLE 0
#define LV_USE_THEME_MONO 1
/* 绘制相关:1bpp 无需 GPU/抗锯齿 */
/* 复杂绘制必须开启:主界面顶部开口浅 V 装饰含斜线,lv_line 斜段依赖 LV_DRAW_SW_COMPLEX,
* 关闭则两腿不渲染(只画水平线)。原值靠 lv_conf_internal 默认(=1)隐式生效,此处显式固化。
* 注意:LV_DRAW_SW_CIRCLE_CACHE_SIZE 须非零(默认 4),否则零长数组编译报错。 */
#define LV_DRAW_SW_COMPLEX 1
#define LV_DRAW_SW_YIELD 0
#define LV_DRAW_SW_ASYNC 0
/* 简单绘制层缓冲:9.3 默认 24 KB(从 LVGL 池里取),本项目 UI 不建层 →
* 与 simulator 一致显式设 0(golden 回归已在 0 下逐字节验证) */
#define LV_DRAW_LAYER_SIMPLE_BUF_SIZE 0
/* 注:9.3 没有 LV_DRAW_SW_YIELD / LV_DRAW_SW_ASYNC(9.4 才有),原两行无效,已删。 */
/* 输入设备:启用键盘/编码器(远程按键走 keypad) */
#define LV_USE_KEYBOARD 0
#define LV_USE_MOUSEWHEEL 0
#define LV_USE_ENCODER 0
/* 软件渲染器的颜色格式支持(F-1,2026-10-11):显示为 I1(LV_COLOR_DEPTH 1),
* 只有 blend_to_i1 会被用到。此前 11 个宏全靠 lv_conf_internal 默认(=1)隐式生效,
* 把 7 个用不到的混合核整份链进镜像:实测 blend_to_* 合计 40,582 B,其中 i1 仅 5,272 B
* → 死代码 35,310 B。每份 blend 文件都是文件级 #if 守卫,关掉 = 整个 TU 不参与编译。
* ⚠ 字形 A8 位图是作为 **mask** 传入的,混合按**目的**格式(I1)分派 → 不需要
* SUPPORT_A8/L8/AL88(此判断已由 simulator 的 I1-only 配置 + golden 回归验证)。
* 取值与 simulator/lv_conf.h 保持一致(模拟器早已是 I1-only)。 */
#define LV_DRAW_SW_SUPPORT_I1 1
#define LV_DRAW_SW_SUPPORT_L8 0
#define LV_DRAW_SW_SUPPORT_AL88 0
#define LV_DRAW_SW_SUPPORT_A8 0
#define LV_DRAW_SW_SUPPORT_RGB565 0
#define LV_DRAW_SW_SUPPORT_RGB565_SWAPPED 0
#define LV_DRAW_SW_SUPPORT_RGB565A8 0
#define LV_DRAW_SW_SUPPORT_RGB888 0
#define LV_DRAW_SW_SUPPORT_XRGB8888 0
#define LV_DRAW_SW_SUPPORT_ARGB8888 0
#define LV_DRAW_SW_SUPPORT_ARGB8888_PREMULTIPLIED 0
/* 不使用的第三方扩展 */
#define LV_USE_PNG 0
/* 输入设备:全部不用(远程按键经 RemoDispBus 直接注入,见 ui_net_sink)。
* 注:9.3 没有 LV_USE_MOUSEWHEEL / LV_USE_ENCODER(LVGL 8 残留),已删;
* LV_USE_KEYBOARD 原在本文件定义两次,去重(下方控件块保留一处)。 */
/* 不使用的第三方扩展(9.3 的图片解码宏是 LV_USE_LODEPNG / TJPGD,
* 没有 LV_USE_PNG / LV_USE_JPG —— LVGL 8 残留,已删) */
#define LV_USE_BMP 0
#define LV_USE_JPG 0
#define LV_USE_GIF 0
#define LV_USE_QRCODE 0
#define LV_USE_BARCODE 0
@@ -94,18 +134,16 @@
#define LV_USE_GRIDNAV 0
#define LV_USE_FRAGMENT 0
/* 裁剪未用控件,减小体积(menu/label/btn/img 等保留) */
/* 裁剪未用控件,减小体积(本 UI 只用 lv_obj + 自绘,见文件头 F-2/F-3 说明)。
* 注:LV_USE_COLORWHEEL / IMGBTN / METER / TEXTLINE 在 9.3 不存在(LVGL 8 残留),已删。 */
#define LV_USE_ANIMIMG 0
#define LV_USE_ARC 0
#define LV_USE_CHART 0
#define LV_USE_CHECKBOX 0
#define LV_USE_COLORWHEEL 0
#define LV_USE_DROPDOWN 0
#define LV_USE_IMGBTN 0
#define LV_USE_KEYBOARD 0
#define LV_USE_LED 0
#define LV_USE_LIST 0
#define LV_USE_METER 0
#define LV_USE_MSGBOX 0
#define LV_USE_ROLLER 0
#define LV_USE_SCALE 0
@@ -121,6 +159,26 @@
#define LV_USE_CALENDAR 0
#define LV_USE_CANVAS 0
#define LV_USE_IMAGE 0 /* 菜单未用图片控件;lv_menu 已关,无图标依赖 */
#define LV_USE_TEXTLINE 0
/* 核心特性:此前全靠 lv_conf_internal 默认值隐式生效(默认 =1 的整份链进固件),
* 现显式固化为 0,并与 simulator/lv_conf.h 对齐(check_lv_conf_sync.py 校验)。
* 其中 LV_USE_OBSERVER 默认 1 → 固件此前一直白链 lv_observer.c。 */
#define LV_USE_OBSERVER 0
#define LV_USE_FLOAT 0
#define LV_USE_MATRIX 0
#define LV_USE_VECTOR_GRAPHIC 0
#define LV_USE_OBJ_PROPERTY 0
#define LV_USE_GESTURE_RECOGNITION 0
#define LV_USE_COLOR_FILTER 0
#define LV_USE_DRAW_PXP 0 /* NXP 并行像素引擎,STM32 无关 */
#define LV_WIDGETS_HAS_DEFAULT_VALUE 0
/* 以下两组是已关父项(LV_USE_LABEL / LV_USE_THEME_DEFAULT)的子选项,本身惰性,
* 但显式写死可防"将来只在一端启用父项"时的静默漂移 */
#define LV_LABEL_TEXT_SELECTION 0
#define LV_LABEL_LONG_TXT_HINT 0
#define LV_LABEL_WAIT_CHAR_COUNT 0
#define LV_THEME_DEFAULT_DARK 0
#define LV_THEME_DEFAULT_GROW 0
#define LV_THEME_DEFAULT_TRANSITION_TIME 0
#endif /* LV_CONF_H */
@@ -78,7 +78,9 @@ static inline uint8_t /* LV_ATTRIBUTE_FAST_MEM */ get_bit(const uint8_t * buf, i
static inline void * /* LV_ATTRIBUTE_FAST_MEM */ drawbuf_next_row(const void * buf, uint32_t stride);
#if LV_DRAW_SW_SUPPORT_RGB565_SWAPPED /* 本地补丁:与定义处守卫一致,见文件末尾 */
static inline lv_color16_t /* LV_ATTRIBUTE_FAST_MEM */ lv_color16_from_u16(uint16_t raw);
#endif
/**********************
* STATIC VARIABLES
@@ -1271,6 +1273,12 @@ static inline uint8_t LV_ATTRIBUTE_FAST_MEM get_bit(const uint8_t * buf, int32_t
return (buf[bit_idx / 8] >> (7 - (bit_idx % 8))) & 1;
}
/* 本地补丁(2026-10-11,随 F-1 裁剪 LV_DRAW_SW_SUPPORT_* 引入):
* 本 helper 的唯一使用点全部在 `#if LV_DRAW_SW_SUPPORT_RGB565_SWAPPED` 块内
* (本文件 1107/1125/1146/1168/1190 行),但上游 9.3.0 漏了定义处的守卫 →
* 关掉 RGB565_SWAPPED 后 ARMCC 报 #177-D "declared but never referenced"。
* 补上与使用点一致的守卫;升级 LVGL 时若上游已修可去掉。 */
#if LV_DRAW_SW_SUPPORT_RGB565_SWAPPED
static inline lv_color16_t LV_ATTRIBUTE_FAST_MEM lv_color16_from_u16(uint16_t raw)
{
lv_color16_t c;
@@ -1279,5 +1287,6 @@ static inline lv_color16_t LV_ATTRIBUTE_FAST_MEM lv_color16_from_u16(uint16_t ra
c.blue = raw & 0x1F;
return c;
}
#endif
#endif
+10
View File
@@ -962,6 +962,16 @@
<FileType>1</FileType>
<FilePath>..\App\algo\adc_health.c</FilePath>
</File>
<File>
<FileName>prot_set.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\algo\prot_set.c</FilePath>
</File>
<File>
<FileName>prot_elm.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\algo\prot_elm.c</FilePath>
</File>
</Files>
</Group>
<Group>
+4 -4
View File
@@ -211,7 +211,7 @@ docs/
| 文档 | 内容 |
|---|---|
| [`问题记录/项目审查与优化建议报告.md`](问题记录/项目审查与优化建议报告.md) | 项目全景审查:文档/代码/架构/可靠性问题清单(D-1~D-7、N-xx、Px-xx)——**体系层评审报告,非待办池**(待办见 §9.3 `待处理内容.md`) |
| [`问题记录/项目审查与优化建议报告.md`](问题记录/项目审查与优化建议报告.md) | 项目全景审查:文档/代码/架构/可靠性问题清单(未闭环项:D-1、N-xx、Px-xx;**已修复项按「修复即销项」删除**,去处见其变更记录)——**体系层评审报告,非待办池**(待办见 §9.3 `待处理内容.md`) |
### 9.2 环境与部署
@@ -246,19 +246,19 @@ docs/
## 11. 已知文档问题(待清理)
> 来源:《项目审查与优化建议报告》D-3/D-7。
> 来源:《项目审查与优化建议报告》D-3/D-7(两条已销项,正文已移入 [`文档规范.md`](文档规范.md) 与 pre-commit 门禁)。
> **D-3 / D-6 / D-7 已处理完毕**(2026-09-25):全库链接存在性检查通过(66 文档 0 失效)、任务表与
> `Src/freertos.c` 一致、文档规范已建立。下表保留**仍需跟踪**的项。
| 问题 | 位置 | 说明 |
|---|---|---|
| 指向未实现工具 | `UI_界面制作流程.md`、`UI_自动化测试流程.md` → `tools/ui_mock/` | 文档承诺了未实现的 UI 渲染/比对工具(其引用已按 §3.3 标注「已删除」状态词) |
| 指向未实现工具 | `UI_界面制作流程.md`、`UI_自动化测试流程.md` → `tools/ui_mock/` | 该目录**始终不存在**;golden 回归实际落在 `simulator/tests/`(四脚本)。引用待改指,见报告 §6.1 |
| 产物缺失 | `内存管理/RAM使用分析与优化方案.md` → `scripts/ram_baseline.tsv` | 方案写了、工具未建(引用已加 `[计划]` 标记) |
| ~~目录名含空格~~ | ~~`UI 设计/`~~ | ✅ 已修复(2026-09-25,D-6):改名为 `UI设计/`,全仓库 67 处引用同步;命名约定见 [`文档规范.md`](文档规范.md) §1 |
| ~~缺一致性门禁~~ | — | ✅ 已落地(2026-09-25/26,D-7 三档齐):`scripts/check_doc_links.py`(存在性)+ `scripts/gen_task_table.py`(数值)+ `scripts/check_mem_numbers.py`(体积),接入 pre-commit `doc-links` / `task-table` / `mem-numbers` 钩子 |
| ~~`docs/文档规范.md` 未创建~~ | — | ✅ 已创建(2026-09-25):[`文档规范.md`](文档规范.md) |
> 完整清单与建议见 [`问题记录/项目审查与优化建议报告.md`](问题记录/项目审查与优化建议报告.md) §3.3。
> 未闭环的文档类问题见 [`问题记录/项目审查与优化建议报告.md`](问题记录/项目审查与优化建议报告.md) §3.1(体量与 D-1)与 §3.4(复核后仅剩三条:D-1、语义断链、元信息漂移)。
---
+8 -3
View File
@@ -25,7 +25,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
| 步 | 脚本/命令 | 门禁内容 | 预期输出 |
|---|---|---|---|
| 1 | `simulator/tests/check_menu_rows.py` | 菜单 8 状态:选中行白带 / 非选行黑底白字 / 框体 1px 边框 / 往返像素一致(S3==S1、S6==S1、S7==S2)+ S0 池基线 | `PASS: 8 状态全部断言通过(8 图,池基线 4784 B)` |
| 1 | `simulator/tests/check_menu_rows.py` | 菜单 8 状态:选中行白带 / 非选行黑底白字 / 框体 1px 边框 / 往返像素一致(S3==S1、S6==S1、S7==S2)+ S0 池基线 | `PASS: 8 状态全部断言通过(8 图,池基线 5368 B)` |
| 2 | `simulator/tests/check_content_pages.py` | ① 文本级断言(字形模板匹配,50 条,独立于 `ref/`)→ ② 11 页截图与 `ref/` 黄金参考**逐字节**比对 + 开页-关页往返池水位 | `PASS: 11 内容页 + 往返池水位全部通过`(文本断言先于像素比对,失败即中止) |
| 3 | `keil-build` skill(`keil_build.py`) | MCU 端编译(UI 源码双端共用,改一端必须过另一端) | 退出码 0 = BUILD_OK |
| 4 | `scripts/check_coding_standard.py` | 代码规范 V1.0 机械检查 | PASS |
@@ -44,8 +44,12 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
| 常量 | 所在脚本 | 当前值 | 含义 |
|---|---|---|---|
| `MENU_POOL_BASELINE` | check_menu_rows.py | **4784 B** | 菜单根框建完的 LVGL 池 used(x64 模拟器) |
| `POOL_AFTER_ROUNDTRIP` | check_content_pages.py | **4864 B** | 开页-关页往返后水位 = 基线 + 一次性残留 80 B |
| `MENU_POOL_BASELINE` | check_menu_rows.py | **5368 B** | 菜单根框建完的 LVGL 池 used(x64 模拟器) |
| `POOL_AFTER_ROUNDTRIP` | check_content_pages.py | **5448 B** | 开页-关页往返后水位 = 基线 + 一次性残留 80 B |
> 水位历史:4784 →(2026-09-14 待机模块常驻 +656)5440 / 5520 →(2026-10-11 裁剪 LVGL 控件类 −72)5368 / 5448。
> 后一次漂移已核实为**有意且更优**:像素逐字节未变(11 页黄金参考在裁剪**前**生成、裁剪后全部一致),
> 且一次性残留仍是 80 B(5448 − 5368)→ 泄漏特征未变,只是 `lv_theme_apply` 不再为已不存在的控件类登记样式条目。
**漂移处理**:菜单/页面结构或样式变更导致门禁失败时,先确认新值合理(残留分解见
`菜单页面设计.md` §9.5),再更新脚本常量并在本文变更记录登记。**禁止为过门禁盲目改常量**——
@@ -102,3 +106,4 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
| 2026-09-10 | 新增 `simulator/tests/regen_ref_png.py`(由 `ref/*.bin` 批量重生成目检 PNG,默认 `--scale 6 --invert`);`ref/*.png` 加入 `.gitignore`(本地调试产物不入库);§四 补"重生成目检 PNG"步骤(修复"刷了 `.bin` 参考但 `.png` 仍是旧渲染 → 看似乱码") |
| 2026-09-10 | `simulator/tests/ref/` **整目录不入库**(`.bin` 黄金参考 + `.png` 目检图均为本地生成物):`git rm --cached` 原有 3 个 `.bin`,`.gitignore` 改为忽略整目录;首次/换机后先 `--update` 生成参考;`--update` 先跑文本断言防误刷;§四 重写 |
| 2026-09-13 | 管线同步:`UI_界面制作流程.md` 移除 HTML 样机阶段(五阶段 → 四阶段 P0~P4),本文定位改为"P2/P3 门禁的可执行版本";§六 关系描述更新为 P0 规格 → P1 编码 → P2 本文回归 → P3 上机目检 → P4 归档;移除 `tools/ui_mock/` 引用 |
| 2026-10-11 | **裁剪 LVGL 死代码(F-1/F-2/F-3)时的门禁记录**:先用**未裁剪**配置 `--update` 生成 11 页黄金参考,再切到裁剪配置重编重跑 → **逐字节全部一致**(菜单 8 状态像素一致),证明裁剪零视觉影响。池水位门禁随之 5440/5520 → **5368/5448**(−72 B;一次性残留仍 80 B,故非泄漏变化,见 §三 水位历史)。顺带揪出并修掉 `Lib/lv_conf.h` 与 `simulator/lv_conf.h` 的**长期漂移**(模拟器早已 I1-only、MCU 端未裁)→ 规则见 `UI_设计架构.md` §1.4 第 10 条,量化见 `../内存管理/RAM使用分析与优化方案.md` §2.6.4 |
+10 -1
View File
@@ -23,7 +23,7 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓
| `Drivers/BSP/UI/ui_net_sink.c/h` | **共用** | RemoDispBus 服务端(收键 + 回送帧缓冲),MCU/PC 双端同一份;平台后端 `net_socket` + `ui_net_sink_backend`(见 RemoDispBus 协议文档) |
| `Drivers/BSP/UI/ui_font_cn_12.c` | **共用** | 中文 1bpp 子集字体(`lv_font_conv` 生成物) |
| `App/task/ui_task.c/h` | MCU | UI 任务:等 NET_READY → 初始化 → 10ms 刷新循环 |
| `Lib/lv_conf.h` | MCU | LVGL 配置(1bpp / 40KB 内存池 / 默认字体) |
| `Lib/lv_conf.h` | MCU | LVGL 配置(1bpp / 40KB 内存池 / 默认字体);⚠ **必须与 `simulator/lv_conf.h` 同一特性子集**,见 §1.4 第 10 条 |
| `simulator/`(main.c / sim/ / lv_conf.h / CMakeLists.txt) | PC | SDL2 模拟器(替代 ui_port,共用全部 UI 源码);支持 `--screenshot` / `--keys <seq>` 无头截图验证 |
| `tools/font_gen/`(extract_cn.py / make_font.py)+ `ui_cn_chars.txt` | 工具 | 字符集提取 / 字体生成 |
| `tools/menu_gen/gen_menu.py` + `ui_menu_tree.txt` | 工具 | 菜单声明源 → 生成 ui_menu_tab.c/h |
@@ -64,6 +64,15 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓
`lv_display_set_theme()` + 对**活动屏幕**再 `lv_theme_apply()`(实测 `bg_before=F5F5F5` → `bg_after=000000`)。
另外 I1 缓冲头 8 B 调色板 LVGL 不替调用方初始化(上电全 0 = 两项透明黑),须显式写 `[0]=黑 [1]=白`;
渲染器本身按亮度阈值置位(`lv_draw_sw_blend_to_i1.c`:亮 → 位=1),与客户端"位=1 即亮"的约定一致。
10. **两份 `lv_conf.h` 必须是同一特性子集(2026-10-11 定;F-1/F-2/F-3 落地时暴露的漂移)**:`Lib/lv_conf.h`(MCU)与 `simulator/lv_conf.h`(PC)是**两份独立文件**,此前已漂移——模拟器早已 I1-only,MCU 端却让 11 个 `LV_DRAW_SW_SUPPORT_*`、5 个未用控件类、`LV_USE_THEME_SIMPLE`、`LV_USE_FLEX/GRID` 全靠 `lv_conf_internal` 默认值隐式生效,把用不到的代码整份链进镜像 = **实测 64,576 B 死代码(旧镜像的 23.5%)**。现两端对齐为同一子集:
* 绘制核只留 `LV_DRAW_SW_SUPPORT_I1`(显示是 1bpp;字形 A8 位图是作为 **mask** 传入、混合按**目的**格式分派,故不需要 A8/L8/AL88);
* 控件全关(`label`/`button`/`buttonmatrix`/`bar`/`textarea`/`image`/`keyboard`)——全仓(`Drivers/BSP/UI`、`App`、`simulator`)**零控件构造函数**,只用 `lv_obj_create` + `lv_theme_mono_init`;
* `LV_USE_THEME_SIMPLE 0`:它是把上述控件类拉进镜像的**锚点**(`lv_display_create()` 会强链 `lv_theme_simple_init`);⚠ 9.3 **没有** `LV_USE_THEME_BASIC`,旧配置里那条是无效项,已删;
* `LV_USE_FLEX 0` / `LV_USE_GRID 0`:全仓无 `lv_obj_set_flex_*` / `lv_obj_set_grid_*` 调用。
⚠ 裁剪后 ARMCC 报 `#177-D`(上游 9.3.0 在 `lv_draw_sw_blend_to_i1.c` 漏了 `lv_color16_from_u16` 的定义处守卫)→ 本仓已按使用点补 `#if LV_DRAW_SW_SUPPORT_RGB565_SWAPPED`(原型 + 定义两处),升级 LVGL 时若上游已修可去掉。
⚠ 将来引入控件型页面(U2 原型库)时**两端同步**逐项打开并复跑 `ui-verify`。量化与验证记录见 `../内存管理/RAM使用分析与优化方案.md` §2.6.4。
✅ **已入门禁**(2026-10-11):`scripts/check_lv_conf_sync.py` = pre-commit 钩子 `lv-conf-sync`。比对的是**生效值**(文件未定义则取 `lv_conf_internal.h` 兜底默认),并抓"写了 LVGL 不认的宏名"与同文件重复定义。首次跑即揪出两端共 **26 个 9.3 不存在的选项**(LVGL 8 / 9.4 模板残留,与 `LV_USE_THEME_BASIC` 同类)、**3 处同文件重复定义**、**10 项 MCU 端靠默认值隐式生效的开关**(含默认 =1 的 `LV_USE_OBSERVER` / `LV_USE_DRAW_PXP` / `LV_USE_IMAGEBUTTON`)——已全部清理/显式固化。
✅ **门禁触发范围**(2026-10-11):`lv-conf-sync` 的 `files:` 同时匹配 `Lib/lv_conf.h`、**`simulator/lv_conf.h`** 与脚本自身 → **两端任一改动都会跑**。此前顶层 `exclude` 整目录挡掉 `simulator/`,模拟器侧改动拦不到;已删该条并把 `simulator/` 下自研代码整体纳入 pre-commit(cppcheck 当天即揪出 `sim_syslog.c` 一处真实空指针解引用),详见 `../代码规范/pre-commit使用说明.md` §5.5。
---
+34 -3
View File
@@ -21,6 +21,7 @@ git commit
├─ doc-links 文档链接存在性(D-3/D-7,.md)
├─ task-table FreeRTOS 任务表一致性(D-2/D-7)
├─ mem-numbers 内存/Flash 数字一致性(D-7 体积档)
├─ lv-conf-sync 两份 lv_conf.h 特性子集一致性(比对生效值)
├─ cubemx-survival CubeMX 重生成存活审计(易失定制清单 + 基线漂移)
├─ watchdog-unit-test 看门狗判定状态机主机侧单测(L0.5)
└─ test-suite-switch 测试套件开关=产品构建(防测试固件静默提交)
@@ -162,7 +163,7 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
| S8 | 4.1.1 | 禁止 Tab 缩进(4 空格) |
| S9 | 5.3 | 函数定义必须有函数注释(含「函数功能」) |
| S10 | 3.3.1 | 函数名必须全小写(HAL_ 厂商 API 回调名豁免) |
| S11 | 2.3.5 | 禁止相对/绝对路径 include |
| S11 | 2.3.5 | 禁止相对/绝对路径 include(2026-10-11 细化判定形态:禁的是"用路径找文件"——引号 include 带 `/`、任意形态带 `..` / `\` / 绝对路径或盘符;`<sys/stat.h>` 这类**尖括号系统头允许带子目录**,由 `-I` 解析。原实现把任何含 `/` 的 include 一律判违规,误伤模拟器侧 2 处;全仓库复核:细化后仅这 2 处项目代码判定发生变化,其余 100 处均在已排除的 `Lib/lvgl/`) |
| S12 | 7.6 | 禁止可变参数函数(`...`) |
| S13 | 7.7 | 禁止递归函数 |
| S15 | 6.1.4 | 文件作用域指针变量必须初始化为 NULL |
@@ -188,7 +189,8 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
**豁免**(不算失效):`[计划]` 前缀;该行含 `[示例]` 标记(引用报错样例/日志片段时用);引用**附近 ±40 字符**内的状态词(未创建/尚未创建/已删除/待排期);占位符(`*`、`<模块>`、`NN_`、`${...}`);**行内代码 span 里的链接写法**(`![标题](相对路径.svg)` 这类语法示例);外部 URL 与纯锚点;`docs/芯片手册/` 等外部资料目录。
**排除文件**:`.opencode/node_modules/`、`Lib/lvgl/`、`simulator/`、`MDK-ARM/RTE/`、`docs/芯片手册/`。
**排除文件**:`.opencode/node_modules/`、`Lib/lvgl/`、`MDK-ARM/RTE/`、`docs/芯片手册/`。
(`simulator/` 已于 2026-10-11 从排除列表移除——其下 3 个 README 是本项目文档,见 §5。)
```powershell
# 全库扫描(开发时自检)
@@ -318,6 +320,20 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
| **典型报错** | `T1 Title exceeds max length (159>100)` |
| **修复方式** | 精简 commit message 标题到 100 字符以内,或拆分为多行(标题+空行+body) |
### 2.15 lv-conf-sync — 两份 `lv_conf.h` 特性子集一致性(2026-10-11 加)
| 项 | 说明 |
|---|---|
| **背景** | `Lib/lv_conf.h`(MCU)与 `simulator/lv_conf.h`(PC)是**两份独立文件**,长期漂移:模拟器早已 I1-only,MCU 端却让 11 个 `LV_DRAW_SW_SUPPORT_*`、5 个未用控件类、`LV_USE_THEME_SIMPLE`、`LV_USE_FLEX/GRID` 靠 `lv_conf_internal.h` 默认值隐式生效 → 固件白链 **64,576 B 死代码**;更糟的是模拟器 golden 回归跑的是另一套特性子集,**验证不到固件实际编译的配置** |
| **触发条件** | `Lib/lv_conf.h` 或 `scripts/check_lv_conf_sync.py` 变动 |
| **判定口径** | 比对**生效值**:文件里没定义的宏,取 `Lib/lvgl/src/lv_conf_internal.h` 的兜底默认。⚠ 该文件每个选项都先套一层 `#ifdef LV_KCONFIG_PRESENT` → `CONFIG_LV_*` 分支**不是**默认值,脚本取块内**非 `CONFIG_` 的最后一个字面量**(首版按"紧跟 `#ifndef` 的那行"取,结果整表都是 `CONFIG_*`,教训) |
| **四类问题** | `MISMATCH` 生效值不同 → FAIL;`INVALID` 写了 LVGL 不认的宏名 → FAIL(`LV_USE_THEME_BASIC` 同类);`DUPLICATE` 同文件定义两次 → FAIL(后者生效,极易误读);`IMPLICIT` 一端靠默认值 → 警告(默认值随 LVGL 版本变,建议显式固化) |
| **端专属白名单** | SDL2 驱动 / 演示工程 / include 替换 / 调试辅助(`LV_USE_OBJ_NAME` 等)/ 断言(固件关、模拟器开)/ `LV_USE_SNAPSHOT`(模拟器 `--screenshot` 需要)——见脚本 `IGNORE_PREFIX` 与 `ALLOW_DIFF`(后者必须写理由) |
| **典型报错** | `FAIL: N 项不一致` + `[MISMATCH] LV_USE_OBSERVER MCU=1(默认) vs SIM=0(显式)` |
| **修复方式** | 把两端拉到同一生效值(**优先显式写死**,别依赖默认),或在 `ALLOW_DIFF` 里登记理由 |
| **触发范围** | `Lib/lv_conf.h`、**`simulator/lv_conf.h`** 或脚本自身变动 → 两端任一改动都会跑。⚠ 早期版本只能由 MCU 端触发,因为顶层 `exclude` 整目录挡掉了 `simulator/`(已删,见 §5);pre-commit 4.x 又移除了 `always: true`(实测对 `local` 钩子只告警并跳过),所以没有"总是跑"的兜底写法——**必须让文件模式覆盖两端** |
| **首次落地实测** | 两端共清掉 **26 个 9.3 不存在的选项**(LVGL 8 / 9.4 模板残留)、3 处同文件重复定义,并把 10 项 MCU 端靠默认值隐式生效的开关显式固化为 0(含默认 =1 的 `LV_USE_OBSERVER` / `LV_USE_DRAW_PXP` / `LV_USE_IMAGEBUTTON`)。本次清理对镜像体积**中性**(那些 TU 本就不在 uvprojx,或编成 0 字节空对象),价值在防漂移与可读性 |
---
## 3. 日常工作流
@@ -425,13 +441,27 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env
**计划**:Route B —— 全库统一格式化后形成独立基线 commit,再启用 clang-format 钩子。启用时取消 `.pre-commit-config.yaml` 中的注释即可。
### 5.5 顶层 `exclude` 曾整目录豁免 `simulator/`(2026-10-11 已修)
**现象**:改 `simulator/` 下任何文件(含 `simulator/lv_conf.h`)的提交**一个钩子都不跑**——`pre-commit run --files simulator/lv_conf.h` 里所有钩子都是 `(no files to check) Skipped`。
**根因**:顶层 `exclude` 写了 `simulator/`。当初是为了挡 SDL2 预编译库、`build/`、截图产物,但这些**已在 `.gitignore` 里**,本就进不了 git 索引;pre-commit 只看索引中的文件,所以那条豁免唯一实际效果是把**本项目自研的 21 个 tracked 文件**(`main.c`、`sim/*.c|h`、`tests/*.py`、`lv_conf.h`、`dbg_log.h`、3 个 README)整体挡在门禁之外。
**为什么必须修**:模拟器与固件**共用同一套 UI 源码**,且它是 UI 回归唯一的像素级证据来源——它落在门禁外,等于验证链的地基不受检。
**纳入后的实测收益(当天)**:`cppcheck` 立刻揪出 `sim_syslog.c` 一处**真实空指针解引用**(`if (p_epoch == NULL || …) { *p_epoch = 0u; … }`——判空分支里紧接着解引用),另有 2 处 `localtime_s` 未初始化告警、1 处 `px` 可加 `const`;`coding-standard` 报出 61 处(44 处函数左大括号 Allman 风格、8 处缺函数注释、3 处缺文件头注释、2 处 `__` 前缀保护宏、2 处 `sprintf`、2 处 S11 误报),已全部处理。
**顺带修掉的规则缺陷**:S11 原实现把**任何含 `/` 的 include** 判违规,误伤 `<sys/stat.h>` 这类系统头;已按 include 形态细化,判定见 §2.8。
**教训**:`exclude` 只写**真正不该进钩子的路径**。已被 `.gitignore` 覆盖的生成物不需要再挡一遍——写上去只会连带豁免同一目录里的自研代码。
---
## 6. 文件清单
| 文件 | 位置 | 说明 |
|---|---|---|
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(15 个钩子 + 1 个预留) |
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**26 个钩子**,2026-10-11 清点;本文 §2 详解非单测类,主机侧单测 10 个见 AGENTS.md 门禁条) |
| `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) |
| `.cubemx_survival_baseline.json` | 仓库根 | CubeMX 易失定制基线(哈希;`--write` 重建) |
| `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 |
@@ -440,6 +470,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env
| `scripts/gen_task_table.py` | scripts/ | FreeRTOS 任务表生成/校验(D-2/D-7) |
| `scripts/check_mem_numbers.py` | scripts/ | 内存/Flash 数字一致性(D-7 体积档) |
| `scripts/check_cubemx_survival.py` | scripts/ | CubeMX 重生成存活审计 |
| `scripts/check_lv_conf_sync.py` | scripts/ | 两份 `lv_conf.h` 特性子集一致性(比对生效值 + 无效宏名 + 重复定义) |
| `scripts/check_test_suite.py` | scripts/ | 测试套件开关门禁(防测试固件静默提交) |
| `docs/代码规范/嵌入式C语言代码规范_V1.0.md` | docs/代码规范/ | 代码规范原文(S1~S19 规则来源) |
| `docs/代码规范/cppcheck使用说明.md` | docs/代码规范/ | cppcheck 钩子详解(规则/配置/抑制) |
@@ -36,10 +36,10 @@
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| ER_IROM1 | 274,728 B (268.29 KB) | 1,048,576 B (1024 KB) | 26.2% |
| RW_IRAM1 | 91,544 B (89.40 KB) | 131,072 B (128 KB) | 69.8% |
| ER_IROM1 | 210,148 B (205.22 KB) | 1,048,576 B (1024 KB) | 20.0% |
| RW_IRAM1 | 91,536 B (89.39 KB) | 131,072 B (128 KB) | 69.8% |
| RW_CCM | 49,184 B (48.03 KB) | 65,280 B (64 KB) | 75.3% |
| **Flash (ROM 合计)** | **275,608 B (269.1 KB)** | 1,048,576 B (1024 KB) | **26.3%** |
| **Flash (ROM 合计)** | **211,024 B (206.1 KB)** | 1,048,576 B (1024 KB) | **20.1%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
@@ -176,13 +176,19 @@
| 段 | 大小 (B) | 占比(对 1 MB) |
|---|---|---|
| Code (inc. data) | 246,456 | 23.5% |
| RO Data | 15,476 | 1.5% |
| RW 初值 | 728 | — |
| **合计 ROM** | **262,660** | **25.0%**(余 ~786 KB) |
| Code (inc. data) | 194,276 | 18.5% |
| RO Data | 15,872 | 1.5% |
| RW 初值 | 876 | — |
| **合计 ROM** | **211,024** | **20.1%**(余 ~813 KB) |
> 本表随代码变化;权威数字与占比见 §2.1 受管快照(`ER_IROM1` / **Flash (ROM 合计)** 行)。
> 历史快照:2026-09-21 为 Code 234,976 / RO 13,824 / 合计 ROM 249,432(23.8%)。
> 历史快照:2026-09-21 为 Code 234,976 / RO 13,824 / 合计 ROM 249,432(23.8%);2026-10-10 为 Code 258,516 / RO 16,208 / 合计 ROM 275,604(26.3%)。
> **2026-10-11 裁剪 LVGL 死代码(原审查报告 F-1/F-2/F-3,已销项)**:`Lib/lv_conf.h` 显式关掉 10 个 `LV_DRAW_SW_SUPPORT_*`(只留 I1)、5 个未用控件类(label / button / buttonmatrix / bar / textarea)、`LV_USE_THEME_SIMPLE`、`LV_USE_FLEX` / `LV_USE_GRID` → **`ER_IROM1` 274,724 → 210,148 B(省 64,576 B = 旧镜像的 23.5%)**,Flash 占比 26.3% → **20.1%**。
> 对象级对照(`.map` 组件表,Code+RO+RW):`blend_to_*` 40,582 → 1,338(其中 `blend_to_i1` 自身 5,272 → 1,338——它按**源**格式的分支也一并消失);控件类 13,397 → 0;`lv_theme_simple` 866 → 0;`lv_draw_sw_transform` 4,340 → 598;`lv_theme_mono` 1,400 → 1,036;镜像内对象数 227 → 212。实测比原报告估算(54,630 B)多省约 10 KB,差额正是这些"锚点消失后连带消失的分支"。
> ⚠ **配套本地补丁**:`lv_draw_sw_blend_to_i1.c` 的 `lv_color16_from_u16` 上游 9.3.0 漏了定义处守卫(唯一使用点全在 `#if LV_DRAW_SW_SUPPORT_RGB565_SWAPPED` 内)→ 关掉该格式后 ARMCC 报 `#177-D declared but never referenced`(破坏"0 警告"门禁)。已按使用点补同一守卫,升级 LVGL 时若上游已修可去掉。
> 验证:ui-verify 五步全过——**11 页内容黄金参考在裁剪前生成、裁剪后逐字节一致**,菜单 8 状态像素一致,LVGL 池水位 5,440 → 5,368 B(−72 B,样式表更小,一次性残留仍 80 B);`@build` 0 错 0 警;`@test` 真机 PASS。详见 `../UI设计/UI_设计架构.md` §1.4 第 10 条。
> **长效防复发**:已建门禁 `scripts/check_lv_conf_sync.py`(pre-commit 钩子 `lv-conf-sync`)——比对两份 `lv_conf.h` 的**生效值**(未定义则取 `lv_conf_internal.h` 兜底默认),并抓无效宏名与重复定义;首次跑揪出两端共 26 个 9.3 不存在的选项、3 处重复定义、10 项靠默认值隐式生效的开关,已清理(本次清理对镜像体积中性)。详解见 `../代码规范/pre-commit使用说明.md` §2.15。
![Flash 布局](../images/内存布局_FLASH.svg)
@@ -12,7 +12,7 @@
> [`../测试说明/实机测试实施指南.md`](../测试说明/实机测试实施指南.md)(故障注入台架流程)、
> [`../架构设计/项目启动顺序管理.md`](../架构设计/项目启动顺序管理.md)(启动时序与就绪屏障)、
> [`../测试说明/ST-LINK_CLI_复位使用说明.md`](../测试说明/ST-LINK_CLI_复位使用说明.md)(复位语义)、
> [`../问题记录/项目审查与优化建议报告.md`](../问题记录/项目审查与优化建议报告.md)(N-03 原始登记,已销项)
> [`../问题记录/项目审查与优化建议报告.md`](../问题记录/项目审查与优化建议报告.md)(N-03 的立项出处;该条已修复并按「修复即销项」从报告删除,**完整记录即本文**)
---
@@ -55,7 +55,7 @@
### 1.3 立项前状态(历史,2026-09-29)
当时全库无 IWDG/WWDG 初始化、HAL IWDG 驱动源码不在仓库、`.ioc` 无看门狗条目、无复位原因读取——缺陷登记为 N-03(`../问题记录/项目审查与优化建议报告.md`,已销项)。**该节只作立项背景**:现状是 IWDG 已启用(`.ioc` + `HAL_IWDG_MODULE_ENABLED` + 驱动已挂载),复位原因每次开机都在上报。
当时全库无 IWDG/WWDG 初始化、HAL IWDG 驱动源码不在仓库、`.ioc` 无看门狗条目、无复位原因读取——缺陷登记为 N-03(原登记于《项目审查与优化建议报告》,已修复并按销项删除正文)。**该节只作立项背景**:现状是 IWDG 已启用(`.ioc` + `HAL_IWDG_MODULE_ENABLED` + 驱动已挂载),复位原因每次开机都在上报。
---
@@ -623,7 +623,7 @@ enter/exit 段还能读出"此刻仍在段内已多久"——**永久阻塞的
| `Inc/FreeRTOSConfig.h:165` | `configASSERT` → `vApplicationAssertFailed` |
| `App/task/net_task.c` / `default_task.c` / `sync/time_sync.c` / `NET/lftpd/*` / `GD5F2GQ5UE/gd5f2gq5ue.c` | 心跳与喂狗埋点(见 §5.5) |
> 立项时(2026-09-29)的"仓库里根本没有 IWDG 驱动"一类取证已随实施失效,不再列出;当时的原始登记见 `../问题记录/项目审查与优化建议报告.md` §4.1(N-03,已销项)。
> 立项时(2026-09-29)的"仓库里根本没有 IWDG 驱动"一类取证已随实施失效,不再列出;当时的原始登记在《项目审查与优化建议报告》§4.1,已随「修复即销项」删除——**取证与实施记录以本文为准**。
## 附录 B:备选方案取舍记录
@@ -456,7 +456,7 @@ void adc_cfg_init(void)
**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,217 B 暂态峰值已占 FreeRTOS 堆空闲的一半以上,且与 lftpd 的每命令分配共存——**流式读写(免整文件驻留内存)仍列为后续优化项**,方案见本文 §6.2/§8 与《项目审查与优化建议报告》§5.3(原报告 §4.4.1 已随 N-01 修复按销项删除,完整记录沉淀在本文)。
## 9. 兼容与迁移
+16 -13
View File
@@ -4,7 +4,7 @@
> **参考来源**:`D:\Code\DTU\参考资料\huaiyuan\huaiyuan\Scatter_P_HV03.01_SV01.01\项目分析文档\04-算法与采样专题\ADC采集至数据使用全流程详解`(同专题另有《项目详细分析-交流采样与有效值计算》《傅氏算法详解》《项目详细分析-阶段6-继电器保护单元分析》)。
> **阅读前提**:先读 `docs/算法/傅氏算法详解.md`(算法原理)与 `docs/硬件平台/ADC通道映射说明.md`(采样通道权威口径)。
> **相关文档**:`docs/产品需求/产品需求设计文档.md`、`docs/待处理内容.md`、`docs/内存管理/RAM使用分析与优化方案.md`。
> **状态**:**P0 采样与相量底座已台架闭环(2026-10-11)**——L1 采集链路(B1,2026-10-10)与 L2 相量层(`sample_ring` + `phasor`,57 项主机单测 + 台架 `@test` 含 `phasor(first)/phasor(hb)` 标记闭环)均已落地;**L3 保护元件 / L4 出口记录 / L5 自检闭锁与定值模型尚未实现**(下一步 = P1,见 §7)。
> **状态**:**P0 采样与相量底座已台架闭环(2026-10-11)**——L1 采集链路(B1,2026-10-10)、L1-6 采样有效性校验与 L2 相量层(`sample_ring` + `phasor`,57 项主机单测 + 台架 `@test` 含 `phasor(first)/phasor(hb)` 标记闭环)均已落地;**P1/P2 保护元件与定值模型软件侧完成(2026-10-11,`prot_set` + `prot_elm`,57 项主机单测)**,板端接线(protectTask)与出口记录(L4)为下一步(见 §7)。
---
@@ -38,7 +38,7 @@
| 已实现子系统 | 网络(CH395F)、存储(GD5F2GQ5UE SPI NAND + dhara FTL + FatFs)、时间(SD2506 RTC + `sys_clock`)、配置(cfg_file)、日志、UI(远端显示)、看门狗(IWDG)、FTP |
| 采样硬件 | TPAFE5160(16 位、8 通道**同步**、±5 V 双极性、并行 `GPIOG[15:0]`)+ STM32 内部 ADC1(12 位、9 通道**顺序**扫描、0~3.3 V 单极性) |
| 采样软件 | `Drivers/BSP/TPAFE5160/tpafe5160.h` 驱动就绪(原子快照 API);`App/task/adc_task.c` **已实现真实消费者(B1,2026-10-10)**——`tpafe5160_snapshot()` 取数 + 帧统计;**采样节拍由 TIM3 更新事件(1 kHz)硬件触发**(2026-10-10;同一事件经 TRGO 触发 ADC1 + DMA);STM32 侧 **`App/algo/adc_input.c` 已实现语义映射 + ADC1 DMA 乒乓帧消费(L1-3/L1-4,v2.0)**,`HAL_ADC_Start_DMA` 已出现 |
| 保护算法层 | **L2 相量层已实现并台架闭环**(`App/algo/sample_ring.c` + `phasor.c`:全周傅氏 5 实例、双区交换、每 1 ms 刷新,2026-10-11 `@test`);**保护元件 / 出口 / 定值模型仍缺失**(P1 起) |
| 保护算法层 | **L2 相量层已实现并台架闭环**(`App/algo/sample_ring.c` + `phasor.c`:全周傅氏 5 实例、双区交换、每 1 ms 刷新,2026-10-11 `@test`);**L3 元件框架 + 过流/过负荷/过压/欠压/零序过压 + 定值模型软件侧完成**(`prot_elm.c` + `prot_set.c`,2026-10-11,57 项主机单测);**出口 / SOE 记录仍缺失**(P4) |
| 已知硬件缺陷 | **N-18**:8 路电气量分属两片转换器 → V0.1 上禁止跨转换器做相位/功率/矢量运算 |
### 1.3 复现范围界定
@@ -277,12 +277,12 @@
| # | 元件 | 判据要点 | 关键定值 | 状态 |
| --- | --- | --- | --- | --- |
| L3-1 | 三段式相间过流 | 相电流基波 > 定值,经延时出口;返回系数滞回 | Ⅰ/Ⅱ/Ⅲ 段电流 + 延时 | ⬜ |
| L3-2 | 三段式零序过流 | 零序电流 > 定值,经延时出口 | Ⅰ/Ⅱ/Ⅲ 段零序电流 + 延时 | ⬜ |
| L3-3 | 过负荷 | 相电流 > 定值,告警/跳闸可整定 | 过负荷电流 + 延时 | ⬜ |
| L3-4 | 过压 / 欠压 | 相/线电压越限,经延时出口 | 过压定值、失压定值 + 延时 | ⬜ |
| L3-5 | 过频 / 低频 | 频率越限,经延时出口 | 过频、低频定值 + 延时 | ⬜ |
| L3-6 | 零序过压 | 零序电压 > 定值 | 零序过压定值 + 延时 | ⬜ |
| L3-1 | 三段式相间过流 | 相电流基波 > 定值,经延时出口;返回系数滞回 | Ⅰ/Ⅱ/Ⅲ 段电流 + 延时 | 🔶 2026-10-11 主机实现:`App/algo/prot_elm.c` 通用"滞回+定时限"核 + `prot_set.c` 定值(Ⅰ/Ⅱ/Ⅲ 段,A 相 `IA`),单测 27 用例含 1.01× 启动/100 ms 到点/滞回保持/返回释放;**板端接线与出口待 protectTask 立项** |
| L3-2 | 三段式零序过流 | 零序电流 > 定值,经延时出口 | Ⅰ/Ⅱ/Ⅲ 段零序电流 + 延时 | ⬜(N-18:`IN` 走 STM32,V0.2 使能;框架已就绪,加实例表一行即可) |
| L3-3 | 过负荷 | 相电流 > 定值,告警/跳闸可整定 | 过负荷电流 + 延时 | 🔶 同 L3-1(`OL-IA` 实例,默认告警不跳闸) |
| L3-4 | 过压 / 欠压 | 相/线电压越限,经延时出口 | 过压定值、失压定值 + 延时 | 🔶 同 L3-1(`OV-UA`/`UV-UA`,欠量方向单测覆盖;沿用参考告警段 0x8027/0x8029) |
| L3-5 | 过频 / 低频 | 频率越限,经延时出口 | 过频、低频定值 + 延时 | ⬜(随频率测量算法 P2;框架已就绪) |
| L3-6 | 零序过压 | 零序电压 > 定值 | 零序过压定值 + 延时 | 🔶 同 L3-1(`3U0-UN` 实例,外部零序电压通道,不受 N-18 限制) |
**扩展集(P2/P3)**
@@ -320,10 +320,10 @@
| # | 功能点 | 说明 | 状态 |
| --- | --- | --- | --- |
| L5-1 | 定值校验和 | `tagPRS_XXX` 的 `dwTagSum` 自检,定值非法闭锁 | ⬜ |
| L5-2 | 采样通道自检 | 通道码值越界/静默检出 | ⬜ |
| L5-1 | 定值校验和 | `tagPRS_XXX` 的 `dwTagSum` 自检,定值非法闭锁 | 🔶 2026-10-11:`prot_set.c` 已实现 sum32 正/反码封存 + selfcheck 失配置闭锁(参考 `dwTagSum/dwTagSumN` 模式);元件级 tagPRS 常量表校验随板端接线并入 |
| L5-2 | 采样通道自检 | 通道码值越界/静默检出 | 🔶 静默/丢帧/序号倒退/错误增量已由 L1-6 `adc_health` 检出(2026-10-11);码值越界待做 |
| L5-3 | 硬件自检 | 出口回路、电源、时钟 | ⬜ |
| L5-4 | 自检失败闭锁 | 任一项失败 → 闭锁相关保护并告警 | ⬜ |
| L5-4 | 自检失败闭锁 | 任一项失败 → 闭锁相关保护并告警 | 🔶 定值闭锁 → 全元件复位已联动(`prot_elm_eval` 读 `prot_set_locked()`,单测覆盖);采样故障闭锁(`adc_health_flags` 电平位 → 元件使锁)随板端接线 |
---
@@ -408,6 +408,8 @@
| 公共 | 返回系数、PT/CT 变比、通道标定系数 | — |
> ⚠️ **待补**:逐项**取值范围 / 步长 / 默认值**取自参考项目 `APP/EquipCfg/Const_CommPara.c` 的 `CommPara_ConfigTable`(`0x8020~0x8882`);本表在实现各保护元件时逐项落实(见 §10.3)。
>
> **2026-10-11 落实进度**:`App/algo/prot_set.c` 已建 RAM 模型(Q10 定点,31 项),**通讯地址逐项对照参考 `Comm/Pub/Const_CommSetPara.c` `g_tCommSetParaTab`**(基址 0x8820:过流 Ⅰ/Ⅱ/Ⅲ = 0x8856~0x8861、过负荷 = 0x8871~0x8874、零序过压 = 0x888E~0x8890;过压/欠压沿用告警段 0x8027~0x802A)。⚠ **默认值/范围为拟定值**(参考 EEPROM 出厂默认表不在本仓库,按 DTU 馈线典型量级拟定),待对照参考出厂表逐项修订。
---
@@ -464,8 +466,8 @@
| 阶段 | 名称 | 内容 | 前置 | 里程碑 |
| --- | --- | --- | --- | --- |
| **P0** | 采样与相量底座 | B1(周期 CONVST + `adcTask` 消费 + 建任务 + 栈预算)+ L1 + L2 | B0 + B1 ✅ | 每 1 ms 产出正确的基波相量 —— **✅ 2026-10-11 台架闭环**(L1-6 同日落地 ✅;L1-5 标定 / L1-7 等周波为 P0 遗留项,精度复核依赖标准源) |
| **P1** | 基础保护元件 | **V0.1 有效子集**:过流(A 相,`IA`)、过负荷(A 相)+ 定值模型;B/C 相过流**建接口不使能** | P0 | 标准源注入 A 相可正确动作 |
| **P2** | 电压/频率类保护 | 过压/欠压(`UA`)、过频/低频、零序过压(`UN`) | P0 | 电压/频率越限正确动作 |
| **P1** | 基础保护元件 | **V0.1 有效子集**:过流(A 相,`IA`)、过负荷(A 相)+ 定值模型;B/C 相过流**建接口不使能** | P0 | 标准源注入 A 相可正确动作 —— 🔶 **软件侧 2026-10-11 完成**(`prot_set` 定值模型 30 用例 + `prot_elm` 元件框架/过流三段/过负荷 27 用例,主机全绿);板端接线(protectTask 立项)与标准源注入待做 |
| **P2** | 电压/频率类保护 | 过压/欠压(`UA`)、过频/低频、零序过压(`UN`) | P0 | 电压/频率越限正确动作 —— 🔶 过压/欠压/零序过压随 P1 同批主机完成(同框架实例);**过频/低频待频率测量算法** |
| **P3** | 异常检测与闭锁 | L5 自检闭锁、采样通道自检;三相类(涌流/PT·CT 断线/母线接地)**建接口不使能** | P1/P2 | 自检失败闭锁;未使能元件不误动 |
| **P4** | 出口、记录与定值集成 | L4 出口与记录、§5 定值模型落 cfg_file、UI 显示、上送 | P1 | 动作出口 + SOE 记录 + UI 显示闭环 |
| **P5** | 方向/功率类(延后) | L3-13 方向元件等 | V0.2 硬件 / 子板计量 | 跨转换器相位一致后实现 |
@@ -603,3 +605,4 @@ B0 产品定义 ✅(2026-10-10 已确认,见 §1.5)
| 2026-10-10 | **L1-1/L1-3 节拍源与搬运方式升级(v2.8)** | 节拍源 **TIM6 → TIM3**(更正硬事实:F4 上 ADC1 的外部触发清单里没有 TIM6/TIM7 TRGO,只接 DAC),TIM3 更新事件同时触发 TPAFE CONVST 与(经 TRGO)ADC1 一轮 9 通道;L1-3 由"软件触发 + 逐通道轮询"改为 **DMA2_Stream4 循环搬运 + 乒乓双缓冲**,`stm32_scan()` → `stm32_take()`,单拍 `adcp` **122 µs → 8 µs**。§1.4 采样软件行、§3.2 L1-1/L1-3/L1-4 同步。完整设计与实测见 `docs/子系统设计/ADC采样节拍设计说明.md` §11 |
| 2026-10-11 | **L2 台架闭环:P0 收尾** | `@build` 0 错 0 警 → `@test` 闭环把 `phasor(first)`/`phasor(hb)` 列为 Required 并校验顺序 `App init OK>phasor(first)>phasor(hb)`(⚠ `net init: OK` **不在**该序内——adcTask 与 netTask 并行,`phasor(first)` ≈+500 ms 早于 `net init: OK` ≈+574 ms 属正常)→ **PASS**:5 实例相量开机即产出、心跳 5 实例持续刷新、`frames` 每 10 s +10000(1000.0 Hz)、`idle=0`、`frst_err=0`、`adc1_err=0`、`adc1_hw=0`、`fw build` 身份戳匹配、回读一致、锚点 ×1、零 `[ERR]`/HardFault;证据 `boot_log_data/20261011_prod_phasor_boot_log.txt`。栈复测:adcTask 静态 832 B / 实测峰值 **928/2048(余 54%)**,7 任务全 PASS(RAM 方案 §2.3 已同步)。§3.1 L1-9 置 ✅(`C04-IA-F2`/`C05-IA-F5` 台架产出);§3.2 L2-1~L2-4/L2-6/L2-7 置 ✅(精度判据保留"待标准源"注),**L2-5 更正为"V0.1 不适用、随 V0.2"**(跨转换器合成被 N-18 禁止、零序走外部专用通道——原表下注"L2-1~L2-7 实现完成"对 L2-5 系误记);§7.1 P0 里程碑置 ✅(遗留 L1-5/L1-6/L1-7)。下一步 = **P1**(A 相过流/过负荷 + 定值模型)与 **L1-6 采样有效性校验** |
| 2026-10-11 | **L1-6 采样有效性校验落地** | 新增 `App/algo/adc_health.c/h`(**纯逻辑判定层**,零平台依赖免桩单测):① 静默——两侧帧序号连续 ≥100 ms 无前进(电平位,可自愈;`adc_health_flags()` 为 L5-4 闭锁预留);② 丢帧——TPAFE 序号一次前进 >1 计 `tgap`(与 STM32 侧 `adc1_gap` 对称,边沿事件不告警——消费者超拍是已知现象,告警会淹没真故障);③ 单调性——序号倒退锁存位;④ 错误增量——FRSTDATA / HAL 错误回调 / STM32 停摆计数增量(事件位与锁存位**编号对齐**,消费者按位图"每类每次开机留痕一次")。`adc_task.c` 只做告警编排:严重事件/上升沿 `DBG_ERROR` + `SYSLOG_SELFTEST`("adc sampling fault"),下降沿 `DBG_INFO`,心跳行增 `tgap`。主机单测 `test/host/adc_health/` **31 用例全绿**(100 ms 门限两侧、tick 回绕、边沿语义、NULL 守卫、init 幂等;`adc-health-unit-test` 钩子登记)。台架 `@test` PASS:零 `[ERR]`(健康不误报)、`tgap=1→2` 与 `adc1_gap` 同步可见、`frames` 1000.0 Hz;证据 `boot_log_data/20261011_prod_adc_health_boot_log.txt`。⚠ **抬栈**:`adc_health_report`→`syslog_post` 控制台回显尾使 adcTask 静态最坏链 832→**1080 B**(512 words 余 968 B < 1024 B 底线)→ **512→640 words**,复测余 **1480 B / 57%**、堆 min_ever 7152→6640 B(AGENTS 任务表 / RAM 方案 §2.3 已同步)。§3.1 L1-6 置 ✅ |
| 2026-10-11 | **P1/P2 软件侧:定值模型 + 保护元件框架** | 新增 `App/algo/prot_set.c/h`(**定值模型**:Q10 定点与参考 2^10 口径一致;31 项元数据表**通讯地址逐项对照参考 `Comm/Pub/Const_CommSetPara.c`**——过流 Ⅰ/Ⅱ/Ⅲ = 0x8856~0x8861、过负荷 = 0x8871~0x8874、零序过压 = 0x888E~0x8890、过压/欠压沿用告警段 0x8027~0x802A;范围校验 + 整表装载 all-or-nothing + **sum32 正/反码封存自检闭锁**(参考 `dwTagSum/dwTagSumN` 模式,L5-1);⚠ 默认值/范围为**拟定**,参考 EEPROM 出厂表不在本仓库,待对照修订)+ `App/algo/prot_elm.c/h`(**元件框架**:通用"越限 + 返回系数滞回 + 定时限"核,数据驱动实例表 7 行 = OC1/OC2/OC3/OL(IA)+ OV/UV(UA)+ 3U0(UN);`prot_elm_eval()` 纯判据可主机单测,`prot_elm_run()` 为 phasor→测量薄适配(相量未就绪整拍跳过);定值闭锁 → 全元件复位)。主机单测 `test/host/prot_set/`(30 用例)+ `test/host/prot_elm/`(27 用例:1.01× 启动 / 100 ms 到点 / 滞回保持与释放 / 欠量方向 / 投退 / 闭锁联动 / 时基回绕)全绿,钩子 `prot-set-unit-test` / `prot-elm-unit-test` 登记。`@build` 0 错 0 警(**未接线 → 链接器整对象移除,零 Flash/RAM 足迹**);板端接线随 protectTask 立项(栈预算 + IWDG 门禁),标准源注入验收随其后。§3.3 L3-1/3/4/6 置 🔶、L3-2/5 注明框架就绪待通道/频率算法;§3.5 L5-1/2/4 置 🔶;§5.4 落实进度注 |
@@ -5,39 +5,44 @@
| 审查日期 | 2026-09-18 |
| 代码基线 | `HEAD = 2d0ec35`(工作树干净,98+ 提交) |
| 固件基线 | `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`,构建于 2026-09-16 12:33,0 Error / 0 Warning |
| **现状复核** | **2026-10-11**,复核基线 `HEAD = 54d6d2d`(分支领先 `origin/v2-on-ui`,未推送);固件基线换为 2026-10-11 00:38 链接的 `.map`。⚠ 复核期间分支又前进(ADC L2 相量层 / L1-6 有效性校验 / `adcTask` 抬栈),其数字影响已在 §5.1"基线口径说明"逐项量化,**不改变任何结论** |
| **销项状态** | 本报告采用**修复即销项**:已修复项的正文与表格行已删除(**874 → 733 行**,114 KB → 84 KB),去处清单见文末**变更记录**;未闭环清单见 **§0.2 / §0.4** |
| 审查范围 | 仓库全量(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` 正向确认已于 2026-10-03 执行并 PASS(2026-10-04 独立复跑 PASS)**,证据见 `子系统设计/配置文件设计说明.md` v1.13。
>
> **销项约定(2026-10-11 起)**:本报告采用与登记簿相同的**「修复即销项」**——问题修复后其分析正文与表格行**从本报告删除**,完整记录沉淀到对应模块文档(去处清单见文末**变更记录**)。因此本报告**只呈现未闭环问题**,不再随时间膨胀。
---
## 0. 执行摘要
### 0.1 六维评分
### 0.1 六维评分(2026-09-18 评级 + 2026-10-11 复核)
| 维度 | 评分 | 一句话结论 |
|------|------|-----------|
| 项目结构 | **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 个有内容页 |
| 维度 | 评分 | 一句话结论 | **2026-10-11 复核** |
|------|------|-----------|---------------------|
| 项目结构 | **B+** | 六层分层真实可执行、命名一致、门禁到位;主要问题是 **BSP 层承载业务模块**、**双构建清单漂移**、**5 个源文件两边都不编译** | **A−**:反向依赖 4→3 处、`net_cfg_legacy` 删除、CI + 22 钩子成型、`App/algo/` 分层补齐;❌ 但"存在但未注册"的**反向校验仍缺**(`check_registration.py` 未建),`rs485_task.c` / `lftpd_log.c` 仍两边不编 |
| 文档分布 | **B-** | 工程文档密度与深度高于同级嵌入式项目;主要问题是 **文档占仓库 60% 体积(其中芯片手册图片 40.5 MB)**、**README 任务表 4× 过期**、**约 18 条断链**、**文档承诺了不存在的工具/台账** | **A−**:工程文档 **28,895 行**(翻倍)、`docs/README.md` 索引 + `docs/文档规范.md` + 三档一致性门禁全部落地,`check_doc_links --all` = **129 文档 0 失效**;⚠ 新风险:AGENTS.md 自身数字开始落后实测("18 钩子"实为 22、"清单 23 项"实为 29) |
| 模块设计 | **B** | 配置引擎、网络串行化、就绪屏障、FatFs 互斥等设计成熟;主要问题是 **失败路径普遍"死循环/静默降级"**、**无看门狗**、**FTP 无鉴权**、**cfg_file 依赖注定失败的 malloc** | **B+**:看门狗(IWDG + 心跳位图 + 4 个 Fault 现场 + `.noinit`)、ADC 全链(TIM3 事件驱动 + DMA 乒乓)、卷锁、墙钟均已落地;❌ **FTP 仍无鉴权**、RS485 仍无协议栈(E-50/E-49 未修)、`Error_Handler` 仍 `while(1)` |
| 内存使用 | **C** | SRAM1 已用 96.6%(余 4,488 B),**64 KB CCM 完全空转**,FreeRTOS 堆被 6 条任务栈吃掉 2/3,**栈/堆水位零观测** | **B**:堆 49,184 B 迁 CCM → SRAM1 降到 **69.8%(余 39,584 B)**、CCM 用 75.1%;栈/堆三面观测常开(`[STACK]` + `stack_report.py` + `configCHECK_FOR_STACK_OVERFLOW=2`);🟡 LVGL 池 40,960 B 未迁也未下调;⚠ 新硬约束:**从堆分配的缓冲不可被 DMA 直接寻址** |
| Flash 使用 | **B+** | 仅用 23.4%;但 **≈50 KB(20% 镜像)是可证明的死代码**(LVGL 配置漂移所致) | **A−**:2026-10-11 落地 F-1/F-2/F-3(两份 `lv_conf.h` 对齐、只留 I1 绘制核、零控件、无 flex/grid)→ **省 64,576 B**,占用降到 **20.1%(211,024 B)**;剩余 F-4/F-5/F-6 ≈10 KB 待逐项实测 |
| UI 标准化 | **B-** | 已有"声明源→生成器→表驱动"的正确骨架;缺**设计令牌层**、**页面原型库**、**注册自动化**、**golden 回归**,42 个菜单叶子仅 7 个有内容页 | **B**:**golden 回归已落地**(`simulator/tests/` 4 脚本:菜单 8 状态全像素 + 内容页逐字节黄金参考 + 协议 e2e);❌ 令牌层 / 组件库 / 页注册表未动,现口径为 **33 叶子 / 6 个有内容页** |
### 0.2 必须优先处理的问题(Top 6)
### 0.2 未闭环问题(Top 5,2026-10-11)
| # | 编号 | 问题 | 影响 | 状态 |
> 已修复/已消解的条目按「修复即销项」从本报告删除,完整记录见对应模块文档(去处清单见文末**变更记录**)。
| # | 编号 | 问题 | 影响 | 现状 |
|---|------|------|------|------|
| 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` 正向确认 PASS(2026-10-03;2026-10-04 复跑 PASS)** |
| 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 |
| 1 | **N-02** | `lftpd.c cmd_pass` 对任意口令无条件回 `230`(现 `:375-379`;`cmd_user :553-557` 同) | 局域网内任意主机可读写 `/config.ini`(含 IP/MAC/网关)→ 可远程断网;单连接 CH395F 上还可挤掉 UI/对时服务 | ❌ 一行未改;⚠ 且**不在 `../待处理内容.md` 登记簿内** → 建议补登 |
| 2 | **N-04 / R-2 剩余** | LVGL 池 `work_mem_int` 40,960 B + UI 帧/绘制 `ui_port.o` 6,420 + UI 发送 `ui_net_sink.o` 6,766 = **54,146 B 仍在 SRAM1** | 堆迁 CCM 后 **CCM 只剩 16,304 B**,这 54 KB 装不下 → R-2 后半段被堵死 | 🟡 必须先做 R-9(`LV_MEM_SIZE` 40 KB→16 KB)或 R-4(帧副本 4→2)腾出空间 |
| 3 | **N-12 / N-13** | `dhara_nand_is_free()` 只查页首 64 B;`dhara_nand_copy()` 不处理 `DHARA_NO_PAGE` 哨兵 | 映射恢复误判 → **数据丢失风险**(低概率、后果重);哨兵页号被当真实页 → 未定义行为 | ❌ 两处原样;⚠ 也不在登记簿 |
| 4 | **N-06 残余** | `rs485_task.c`、`lftpd_log.c` 既不在 uvprojx 也不在 simulator | "写了不编译"= 死代码 + 误导 | ❌ 未处理;❌ `check_registration.py`(S-3)仍未建 |
| 5 | **N-16** | 日志每条记录一次 `f_open + f_write + f_close`(`sys_log_mcu.c:123-140`) | 每条日志 2–4 次 NAND 页读写;UI 翻记录逐条 open/seek/close | ❌ 未办(仅 `FIL` static 化省 528 B 栈) |
⚠ 另有两条**不在上表但需排期**:`Error_Handler` 仍 `__disable_irq(); while(1){}`(N-03c,§4.1)、`.git` 已膨胀到 **149 MB**(D-1,§3.1)。
### 0.3 三条主线建议
@@ -47,6 +52,33 @@
---
## 0.4 未闭环清单(2026-10-11 复核)
> 只列**未办 / 半办**项;已修项已按「修复即销项」删除(去处见变更记录)。图例:❌ 未办 / 🟡 半办。
| 类别 | 编号 | 未闭环内容 | 下一步 |
|------|------|-----------|--------|
| **安全** | **N-02** ❌ | FTP 无条件 `230`,任意主机可读写 `/config.ini` | 补登登记簿 + `[ftp] user/pwd_hash` 定长比较 + 失败计数锁定 + 生产默认只读 |
| 安全 | N-03c ❌ | `Error_Handler` 仍 `__disable_irq(); while(1){}`(`Src/main.c:274-282`) | 统一降级策略:记自检 + 禁用该子系统 + 其余照常 |
| **数据正确性** | **N-12 / N-13** ❌ | `is_free` 只查页首 64 B;`copy` 不处理 `DHARA_NO_PAGE` | 补登登记簿 + 整页(或首/中/尾)扫描 + 返回 `DHARA_E_UNSUPPORTED` |
| 数据正确性 | N-14 🟡 | 写回缓存无空闲/掉电强制 flush(整页写与换页已 flush) | 空闲 N ms 由 defaultTask flush |
| **内存 RAM** | **R-2 剩余** 🟡 | LVGL 池 + UI 两份缓冲 54,146 B 仍在 SRAM1;CCM 只剩 16,304 B | 先做 R-9 或 R-4 腾 CCM,再迁 |
| 内存 RAM | R-3 / R-4 / R-5 / R-6 / R-9 ❌ | 页缓冲未合并 / UI 帧 4 副本 / `configMAX_PRIORITIES` 仍 56 / 栈仍动态 / `LV_MEM_SIZE` 仍 40 KB | R-9 最省力(池水位门禁数据已具备);R-6 前置条件已满足 |
| 内存 RAM | N-11 🟡 | NAND 仍直 DMA 调用方缓冲(已加运行时守卫、阈值 1024) | 补编译期/启动期地址断言 + **新铁律:堆缓冲不可被 DMA 直接寻址** |
| 内存 RAM | §5.5 ❌ | `ram_baseline.tsv`、`RAM预算台账.md`、92% 阈值门禁均未建 | 三条一起补,否则 R-2 后半段无法排期 |
| **Flash** | **F-4 / F-5 / F-6** ❌ | 剩余可评估 ≈10 KB(`box_shadow` 3,246 / `draw_sw_img` 2,960 / `bin_decoder` 1,972 / `image_decoder` 952 / `triangle` 930)+ 双精度 `dadd` 等 ≈1 KB;uvprojx 仍挂 201 个 LVGL `.c`(F-2 后均为空 TU,省的是编译时长) | 逐项实测再关;⚠ `mask`/`mask_rect`/`grad` 是文字与矩形绘制的必经路径,**不可盲关** |
| **性能/资源** | N-09 / N-16 / P2-8 / PHY / P3-x / rs485 ❌ | PASV 每次新建监听;日志每条 open/write/close;`NET_RECV_TIMEOUT_MS 30000`;PHY 强制 100M 无自协商回退;`NET_MAX_SOCKETS 8` 与单连接语义不符;`HAL_Delay(1)` 阻塞 DE | 见 §4.2 / §4.5 / §4.7 各行建议 |
| **结构** | S-1 / S-2 / S-3 / S-4 / N-06 残余 ❌ | `NETCFG`/`UI` 语义层错位;4 处裸 `extern`;**无注册反向校验**;`app_cfg.h` 开关未接线未删;`rs485_task.c`/`lftpd_log.c` 两边不编 | `check_registration.py` 是其中唯一"1 小时工作量"的一条 |
| **文档/仓库** | D-1 ❌ | 图片 36.3 MB 仍在仓;**`.git` 已 149 MB** | 降采样 / 拆 submodule / LFS 三选一 **+ 一次 `git filter-repo` 历史重写** |
| 文档/仓库 | 语义断链 ❌ | 两份 UI 文档仍指 `tools/ui_mock/`(不存在,实为 `simulator/tests/`) | 改指 + 挂进 `ui-verify` |
| 文档/仓库 | 元信息漂移 ⚠ | AGENTS.md 写"18 钩子"(实 22)、"清单 23 项"(实 29) | 纳入生成/校验(同任务表做法) |
| **UI** | U1 / U2 / U3 / U5 ❌ | 无令牌层、无组件库、无页注册表(手写 `case` 仍在);**27 页**未落地 | 按 §6.5 顺序,U4 的 golden 已可当 U1 的验收工具 |
| UI | U4 残余 🟡 | golden 已建但未挂 pre-commit;字符集差集检查仍是约定 | 两个脚本入门禁 |
| **测试** | P1-10 🟡 / 覆盖率 ❌ | CH395F 套件收尾两步;无覆盖率概念;主机侧对 `lftpd`/`dhara` 移植层无测试 | N-12/N-13 这类缺陷至今只能靠人工读代码发现 |
| **台账** | ⚠ | N-02、N-12/N-13 **漏登**;报告 N-18 与登记簿 N-18 **同号不同义** | 补登 + 引用一律带文档名 |
---
## 1. 审查方法与证据基线
### 1.1 数据源
@@ -81,8 +113,8 @@ Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern '__heap
| 术语 | 定义 |
|------|------|
| 自研代码 | `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** |
| Flash 占用 | `Code + RO-data`(基线 245,384 B → **2026-10-11 复核 273,860 B**);`ROM` 再加 `RW-data` 初始化表(246,016 → **274,060 B**) |
| RAM 占用 | `RW-data + ZI-data`(基线 126,584 B,全部压在 SRAM1)。**2026-10-11 复核:140,720 B,已跨两个区域** = SRAM1 `RW_IRAM1` 91,488 / 131,072(**69.8%**)+ CCM `RW_CCM` 49,184 / 65,280(**75.3%**)+ `UNINIT_CCM` 48 B(`.noinit` 崩溃现场保留区)。⚠ 口径变化:原报告"CCM 不在 scatter 内计 0"已不成立 |
| "死代码" | 该对象在镜像中有码,但其功能被工程配置(如 `LV_COLOR_DEPTH 1`)或调用点扫描(零调用者)证明不可达 |
---
@@ -93,41 +125,40 @@ Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern '__heap
| 分类 | 文件 | 行数 | 说明 |
|------|------|------|------|
| `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 |
| `App/`(自研应用) | 38 | 8,516 | 配置引擎、墙钟、日志、任务薄壳;**新增 `App/algo/`(L1 采集 + L2 相量,6 文件 1,090 行)** |
| `Drivers/BSP/`(自研驱动/子系统) | 67 | 19,104 | 其中 **UI 子系统 6,036 行**(含 `ui_font_cn_12.c` 1,211)、NET 6,302、CH395F 2,144、GD5F 1,978、SD2506 835、TPAFE 680、NETCFG 578、RS485 310 |
| `test/`(自研测试) | 41 | 10,855 | **`test/host/` 主机侧单测 25 文件 / 4,505 行**(2026-09-18 时不存在)+ `test/board/` 板级套件 16 文件 / 6,350 行 |
| **自研合计** | **146** | **38,475** | 2026-09-18 基线为 **108 / 24,353** → **+38 文件 / +14,122 行** |
| CubeMX `Src/`+`Inc/` | 26 | 5,459 | 受 USER CODE 标记保护 |
| `simulator/` | 10 | 2,462 | PC SDL2 宿主 |
| 第三方 `Lib/` | 958 | 529,916 | LVGL 9.3(423 个 `.c` / 421,546 行)、FreeRTOS、FatFs、dhara、lftpd |
**评价**:自研体量与第三方体量比约 1:20,属于典型的"薄自研 + 厚中间件"。这决定了**结构审查的重点不在自研代码内部,而在"自研如何约束第三方"**——而 §5.4 会证明这层约束(`lv_conf.h`)目前是失守的。
**评价**:自研体量与第三方体量比约 **1:14**(原 1:20,自研涨了一倍半),仍属典型的"薄自研 + 厚中间件"。这决定了**结构审查的重点不在自研代码内部,而在"自研如何约束第三方"**——§5.4 曾量化这层约束(`lv_conf.h`)失守的代价:**64,576 B 死代码**(2026-10-11 已修)。
### 2.2 分层与依赖方向
实测自研层间的**全部反向依赖**(BSP → App)只有 4 处:
实测自研层间的**全部反向依赖**(BSP → App)——2026-10-11 复核为 **3 处**(原 4 处,`net_cfg_legacy.c` 已随旧档迁移路径整体删除,见 §2.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/NETCFG/net_cfg.c:31` | `cfg_file.h` | 可接受但需说明:NETCFG 实为"业务配置模块",放在 BSP 层是历史命名 |
| `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) | 中间件→应用;已在文件内注释声明为"④中间件层"有意为之 |
| `Lib/FatFs/ffsystem.c:19` | `sys_clock.h`(App/util) | 中间件→应用;已在文件内注释声明为"④中间件层"有意为之 |
**评价:良好**。在 24k 行规模上把反向依赖控制在 4 处,说明分层不是墙上的图。但存在**语义层错位**:
**评价:良好**。在 38k 行规模上把反向依赖控制在 3 处,说明分层不是墙上的图。但存在**语义层错位**:
- `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`)实测 4 处——**2026-10-11 复核:4 处全部仍在**(仅行号漂移),S-2 未办:
| 位置 | 裸 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;` |
| `Drivers/BSP/CH395F/ch395f.c:178` | `extern volatile uint8_t g_spi1_dma_done;`(实为 GD5F 在 `gd5f2gq5ue.c:50` 定义的变量,跨模块反向且**未进任何头文件**) |
| `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c:47` | `extern SPI_HandleTypeDef hspi1;`(`Inc/spi.h:35` 已导出,局部重复) |
| `Drivers/BSP/NET/net_socket.c:56` | `extern osMessageQueueId_t netMsgQueueHandle;` |
| `Drivers/BSP/SD2506/sd2506.c:20` | `extern I2C_HandleTypeDef hi2c1;`(`Inc/i2c.h:35` 已导出,局部重复) |
**建议(S-2)**:CubeMX 句柄类(`hspi1`/`hi2c1`)统一由 `main.h` 导出并 include,删除局部 extern;`g_spi1_dma_done` 应由 GD5F 模块自己定义并在头文件 `extern` 声明(当前跨模块裸 extern 是真正的耦合隐患);`netMsgQueueHandle` 改为 net_socket 内部持有 + 提供投递 API。
@@ -137,11 +168,11 @@ Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern '__heap
| 类别 | 文件 | 后果 |
|------|------|------|
| **两边都不注册** | `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 宏)语义重复 |
| **两边都不注册** | 基线 5 个:`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` | **2026-10-11 复核:剩 2 个**——`crc.c`/`ringbuf.c` 已整体删除(`fac9d1c4`,2026-10-02,全库无调用者);`adc_task.c` 已挂 uvprojx(**仅 MCU**,模拟器不编,符合"任务层不入模拟器"的既有做法);❌ **`rs485_task.c`(等 E-50/E-49 修复)与 `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) |
| **uvprojx** | 306 个 `.c`(含 201 个 LVGL)(基线 290) | LVGL 全量编译 → 编译时长;死代码问题被"全编"放大(见 §5.4) |
**评价**:AGENTS.md 已把"新文件必须注册两处"写成铁律并有 `new-source-file` skill,但**缺少反向校验**("存在但未注册")。这是纯粹的机械检查缺口。
**评价**:AGENTS.md 已把"新文件必须注册两处"写成铁律并有 `new-source-file` skill,但**缺少反向校验**("存在但未注册")。这是纯粹的机械检查缺口——**2026-10-11 复核:该缺口未补**(`scripts/` 现有 11 个脚本,无 `check_registration.py`)。
**建议(S-3,1 小时工作量)**:新增 `[计划] scripts/check_registration.py`,规则:
1. `App/**.c`、`Drivers/BSP/**.c`(排除 `*_sim.c`、`*_mcu.c` 之外的模式白名单)必须同时出现在 uvprojx 与 CMakeLists;
@@ -153,52 +184,32 @@ Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern '__heap
| 观察 | 证据 | 判定 |
|------|------|------|
| 根目录散置工作文件 | `待处理内容.md`(仅 2 行的 RAM 待办)、`ui_menu_tree.txt`、`ui_cn_chars.txt` | 菜单树是**生成器输入**,放根目录合理但需在 `docs/` 有权威说明;`待处理内容.md` 内容已被 `docs/内存管理/` 覆盖 → 建议合并后删除 |
| 根目录散置工作文件 | `ui_menu_tree.txt`、`ui_cn_chars.txt` | 菜单树/字符集是**生成器输入**,放根目录合理且已在 `docs/` 有权威说明(原点名的根目录 `待处理内容.md` 已并入 `docs/待处理内容.md` 统一登记簿) |
| `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 |
| 功能开关名存实亡 | `App/app/app_cfg.h` 的 `APP_NET_ENABLE/APP_ADC_ENABLE/APP_RS485_ENABLE` **全库零引用** | 与台账 **P2-12** 同源。开关不生效比没有开关更糟——评审者会误信"ADC 可裁剪"。**2026-10-11 复核:🟡 已订正表述但未接线**——文件头 v1.1(2026-10-04)已如实标注"预留占位、不门控任何代码",`APP_CCM_ENABLE`/`APP_UI_NET_SINK` 已删除,三个 `APP_*_ENABLE` 仍在且仍零引用 |
### 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 体量画像(实测,工作树)
### 3.1 体量画像(实测,工作树;2026-10-11 复核,括号内为 2026-09-18 基线)
| 对象 | 文件数 | 体积 | 占仓库 |
|------|--------|------|--------|
| 仓库受控文件总计 | 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 行 | — |
| 仓库受控文件总计 | 2,363(2,013) | **77.7 MB**(78.3 MB) | 100% |
| `docs/` 合计 | 849(652) | **44.4 MB**(47.19 MB) | **57.1%**(60.3%) |
| └ `docs/芯片手册/` 图片 | 703 张(564 张) | **36.3 MB**(40.5 MB) | **46.7%**(51.7%) |
| └ `docs/images/引用关系图.json` | 1 | 1,072 KB(1,052 KB) | 1.3% |
| └ `docs/UI设计/mockups` | 15 | 1,110 KB(1,110 KB) | 1.4% |
| `.git` 目录 | 1,027 | **149.3 MB**(46.6 MB pack) | — |
| 工程文档(Markdown,自研) | 77 篇 / **28,895 行**(40 篇 / 14,124 行) | — | — |
| 外部资料(Markdown,datasheet 转录) | 14 篇 / 58,244 行(16 篇 / 59,142 行) | — | — |
**关键失衡**:工程文档 14,124 行 vs 外部资料 59,142 行;**图片体量是全部 Markdown 的 3 倍**。这意味着:
- `git clone` 的净耗时几乎全部花在扫描手册图片上;
- 与工程决策相关的信息(40 篇)在体积上被完全淹没;
- `引用关系图.json`(1 MB)是**生成产物**,与 `.dot/.svg` 一并提交(AGENTS.md 明确要求),但它对阅读无价值、对 diff 有害。
**关键失衡(复核后仍在,但已缓解)**:工程文档行数从 14,124 涨到 **28,895**(翻倍),外部资料行数基本不变 → 比例从 1:4.2 改善到 **1:2.0**;但**图片体量(36.3 MB)仍是全部 Markdown 的 4 倍**,`git clone` 的净耗时依旧几乎全花在扫描手册图片上。⚠ 新增事实:`.git` 已膨胀到 **149 MB**(受控文件才 77.7 MB),说明历史里存过多版图片/产物——**D-1 的优先级因此上升**(拆仓或 LFS 之外,还需 `git filter-repo` 类历史清理才能见效)。
### 3.2 做得好的(应保留并显式制度化)
@@ -218,72 +229,17 @@ graph TD
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`。
> **2026-10-11 复核:未办,且问题变大**。图片增至 **703 张 / 36.3 MB**(受控文件总量 77.7 MB,图片仍占 46.7%);⚠ 新事实:`.git` 目录已达 **149.3 MB**(1,027 个对象文件),远大于工作树受控内容 → 历史里存了多版图片/日志/产物。**只做 1/2/3 中任一项都不足以解决 clone 耗时,还需一次历史重写(`git filter-repo`)**;重写属破坏性操作(所有克隆需重取),须单独排期并先备份裸仓。
### 3.4 文档结论
**内容质量高、分布形态差**。按 D-1..D-7 处理后可从 B- 提升到 A-:工程量约 1 人日(不含拆仓),收益是 clone 时间与"文档可信度"。
> **2026-10-11 复核**:**内容质量侧已到位**(评级 A−,见 §0.1;D-2..D-7 已销项),**分布形态侧未动**——未闭环的文档类问题只剩三条:
> - **D-1 ❌**:`docs/` 仍占仓库 **57.1%**、图片仍 **36.3 MB**,且 `.git` 已膨胀到 **149 MB** → 已从"P1 优化"变成影响日常 clone/备份的实际痛点;单靠拆仓或 LFS 不够,**还需一次历史重写**。
> - **语义断链 ❌**:两份 UI 文档仍指向 `tools/ui_mock/`(该目录从未存在,实现落在 `simulator/tests/`)——路径检查抓不到,属语义层。
> - **元信息漂移 ⚠**:门禁与文档越多,自身数字也会过期——AGENTS.md 现写"18 个钩子"(实测 `.pre-commit-config.yaml` **22** 个)、"清单 23 项"(实测 `check_cubemx_survival.py` **29** 项)。建议把这类计数也纳入生成/校验,否则报告与规范各说一套。
---
## 4. 功能模块设计评审
@@ -299,13 +255,8 @@ D-2/D-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` 零引用 | | 开关要么生效要么删 |
| **N-03c** | 多处初始化失败即挂死(`Error_Handler()` at `main.c:221`;net/ui 初始化失败路径) | | 建议统一降级策略:**外设失败 → 记自检日志 + 该子系统禁用 + 其余照常**(LED/屏上给出降级标识)。**❌ 2026-10-11 复核仍在**:`Src/main.c:274-282` 仍 `__disable_irq(); while(1){}`(IWDG 独立于中断,最终仍会被硬复位,但无降级、无定位信息) |
| P2-12 | `App/app/app_cfg.h` 三个 `APP_*_ENABLE` 零引用 | | 开关要么生效要么删 → **🟡 部分处理**:已如实标注"预留占位、不门控任何代码"(v1.1),未接线也未删 |
### 4.2 网络子系统(CH395F + BSD 抽象层)
@@ -318,13 +269,11 @@ D-2/D-3 的共同根因:**没有任何机械检查**保证"文档说的 = 代
| 编号 | 问题 | 证据 | 建议 |
|------|------|------|------|
| **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 侧背压。**→ 已销项(2026-10-10)**:未走"事件驱动",改用**门控**——`net_can_accept()`/`net_can_read()` 纯 RAM 读控制块,无活时**一次 RPC 都不发**(100 msg/s → 0);实测 `uis` 50 ms → **1 µs**。方案见 `../UI设计/RemoDispBus协议与接收设计说明.md` §3;判读口径与实测基线见 `../子系统设计/看门狗设计说明.md` §11.4 |
| 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" |
| **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` 式的只读模式)。**❌ 2026-10-11 复核:一行未改**(现 `lftpd.c:375-379` / `:553-557`,全文件无 password/auth/attempt/read_only);⚠ 且**未登记进 `待处理内容.md`** → 建议补登为 P0/P1 |
| N-09 | `cmd_pasv` 每次新建监听 `lftpd_inet_listen(0)`(随机端口) | `lftpd.c:373` | 在**单连接 socket** 上与 UI `:7003`、对时 `:8888` 争用同一物理 socket;建议限定监听资源池 + 显式"FTP 会话独占"策略,并在文档写明"一次只允许一个 TCP 服务活动"。**❌ 复核仍在**(现 `lftpd.c:392`,3 次重试 + 200 ms 间隔) |
| P2-8 | `net_recv` 阻塞超时 30 s | 台账 | 缩短并让上层可取消。**❌ 复核仍在**:`net_config.h:39` `NET_RECV_TIMEOUT_MS 30000` |
| — | PHY 强制 100M 全双工,忽略自协商 | AGENTS/architecture | 与某些交换机会联不通;至少加"链路 up 失败 3 次后回退自协商"。**❌ 复核仍在**:`net_task.c:110` 无条件 `ch395f_set_phy(CH395F_PHY_100M_FULL)`,仅 3 s 链路等待 + DISCONN 时上报 `comms fault` 自检 |
| P3-x | `NET_MAX_SOCKETS 8`(`net_config.h:24-25`)是**软件表**,与硬件单连接语义不符 | | 文档已说明,但 API 层看不到限制 → 建议在 `net_socket.c` 断言"同时 LISTEN 数 ≤ N"。**❌ 复核仍在**(现 `net_config.h:27`,值仍 8) |
### 4.3 存储子系统(GD5F2GQ5UE + dhara + FatFs)
@@ -337,47 +286,38 @@ D-2/D-3 的共同根因:**没有任何机械检查**保证"文档说的 = 代
| 编号 | 问题 | 证据 | 影响/建议 |
|------|------|------|-----------|
| **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" |
| **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 失败。**2026-10-11 复核:🟡 模式未变但已加固**——阈值改为 `GD5F_DMA_THRESHOLD 1024`(`gd5f2gq5ue.h:78`),并新增**运行时 DMA 可达性守卫**(`:110-136`,不可达自动回落轮询)。⚠ 复核新发现:堆已迁 CCM(`ucHeap @0x10000020`)→ **"从 FreeRTOS 堆分配的缓冲不可被任何 DMA 直接寻址"成为全局铁律**,比原结论更硬 |
| **N-12** | `dhara_nand_is_free()` 只检查页首 64 B | `nand_ftl.c:137-154` | dhara 依赖该判定做映射恢复;页首 64 B 全 FF 但后半页有数据时会误判为空闲 → 数据丢失风险(低概率但后果重)。建议整页扫描或至少首/中/尾三处采样。**❌ 复核仍在**(现 `nand_ftl.c:152-179`,仍 `uint8_t buf[64]`) |
| **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; }`(返回不支持是安全语义)。**❌ 复核仍在**(现 `:204-216`,read→prog 直传) |
| N-14 | 写回缓存 `s_cache_buf/s_cache_dirty` 无掉电保护策略:`CTRL_SYNC` 已实现(`:343-353`)但**没有周期性或掉电前的强制 flush** | `nand_ftl.c:291-353` | 掉电窗口 = 最后一次未跨页的写。建议:空闲 N ms 由 defaultTask flush + 掉电检测(若硬件支持)或至少"日志落盘后立即 fsync"。**🟡 复核:部分改善**——整页写立即 flush(`:377-381`)、换页时 flush(`:365-368`)、`CTRL_SYNC` 走 flush + `dhara_map_sync`(`:400-406`);仍无空闲定时 flush |
| 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 物理块) |
| 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)。**🟡 复核:已省一半**——`s_bbt_page_buf` 由 2,048 B 收紧为 **272 B**(2026-09-21);`nand_ftl` 三块 2 KB 未合并(对象 ZI 仍 6,216) |
| — | `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`,属主单任务化。这是全项目设计最完整的一个模块。
**设计良好**:描述符 + 段注册表、外科式(surgical)保存、模板生成、2 s 轮询(`cfg_file_poll`)响应 `SYS_EVT_CFG_SAVE`,属主单任务化。这是全项目设计最完整的一个模块。(原"首启旧档迁移"已于 2026-10-10 删除——该一次性路径把整条配置写链挂进启动期调用图,defaultTask 静态最坏链 +600 B。)
**问题**:
| 编号 | 问题 | 建议 |
|------|------|------|
| **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。
> **诚实标注**:本结论由静态分析得出(映射表 + 启动文件 + 调用图三重证据),**尚未上机确认**。`.dsh/skills/serial-boot-test/scripts/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` 零命中——已于 2026-10-03 执行 PASS,2026-10-04 独立复跑再次 PASS:`[CFG] load: OK (523B, heap free 8840B)`、Forbidden `FAIL`/`ERROR`/`HardFault`/`[ERR]` 零命中、`selftest: no fault record`、ST-LINK 全片回读 `No difference found`。)**
| — | 失败即静默回落工厂默认(`:955-988`:缺文件→模板→return -1;超长→模板→return -1;读失败→"factory defaults kept") | 语义本身合理(不阻塞组网),但**用户改了 IP 重启就丢且无告警**——建议回落时必写一条自检记录并在 UI 装置信息页给出"当前为出厂配置"标识 |
### 4.5 日志子系统(`App/log/sys_log_mcu.c`)
**设计良好**:定长 64 B 记录(`SYSLOG_REC_SIZE`)、单写者(defaultTask)、RAM 队列 + `syslog_drain`、容量满时删除重建、`/log` 目录惰性创建。
> **2026-10-11 复核:日志侧新增了两个基线没有的东西,值得记一笔。**
> ① **控制台单写者日志**(`App/log/console_log.c`,对象 ZI 4,120 B)——把"多任务直接 `printf` 到 USART1"收敛为单写者队列,配套 `DBG_*` 宏;代价已量化进 AGENTS.md(**每条日志约 90 B Flash + 216 B 栈**,深调用链模块另以 `#define DBG_LINE_BUF_MAX 128U` 收紧)。⚠ 这条代价模型正是 defaultTask 静态最坏链从 5,312 → 2,776 B 的主要杠杆,**属本报告"内存"章节应当吸收的新事实**。
> ② **自检记录可查**:`selftest.log` + 开机 `selftest: no fault record` 标记 + Fault 现场 `.noinit` 跨复位 → 现场故障从"不可知"变成"可回读"。
**问题**:
| 编号 | 问题 | 证据 | 建议 |
|------|------|------|------|
| **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` 雏形,可升级为页缓存) |
| **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` 雏形,可升级为页缓存)。**❌ 2026-10-11 复核仍在**:`sys_log_mcu.c:123-140` 每条仍 open/write/close(失败重建重试一次),`wdev.log` 同模式(`:481-484`);🟡 唯一改善 = `FIL` 已 static 化(`:70`,省 528 B 栈),**无常驻打开句柄、无批量 flush** |
| P2-9 | `mkdir` 失败后不再重试(`s_fs_ready` 逻辑,`:158-165`) | 台账 | 失败时保持未就绪 + 定期重试 + 上报自检 |
| P2-10 | 容量轮转 `f_unlink`(`:64`)与 UI 读并发 | 台账 | 轮转只在写者、读者持版本号 |
@@ -385,13 +325,15 @@ D-2/D-3 的共同根因:**没有任何机械检查**保证"文档说的 = 代
见 §6 专题(同时是"标准化"章节)。此处仅记设计层结论:三层拆分(数据 `ui_menu_tab` / 视图 `ui_menu_view` / 控制器 `ui_menu`)+ 内容页描述符框架**方向正确**,但**框架未收敛到"新页面零框架改动"**,且 4 份 3,200 B 帧副本占 12.8 KB RAM(§5)。
> **2026-10-11 复核:两条结论均未变化。** 框架仍要求"改 `ui_menu.c` 的手写 `case` + 加实例文件 + 双端注册"才能新增一页(§6.2 的 8 步一字未减);帧副本仍 4 份(`ui_port.o` ZI 6,420 + `ui_net_sink.o` 6,766 = 13,186 B,R-4 未办)。✅ 唯一实质改善在**验证侧**:golden 回归已建(§6.1),UI 改动从此有逐像素/逐字节基线可依赖。
### 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` 未注册 → 无帧校验、无接收环 |
| `tpafe5160`(并行 ADC) | BUSY 超时、EXTI 读取路径齐备;取数已收口为 `tpafe5160_snapshot()` 原子快照(帧序号只增不清 + FRSTDATA 自检) | ⚠ **V0.1 硬件缺陷(登记簿 N-18,V0.2 改版项;注意与本报告 N-18 同号不同义)**:8 路电气量被分给两片转换器 → 主板侧**禁止**跨转换器做相位/功率/功率因数运算;并行总线无批量采样框架,16 位端口读依赖 `gpio.c` 初值 |
| `rs485` | 结构简单 | DE 时序用 `HAL_Delay(1)` **阻塞 1 ms**(在调用任务里);`crc.c`/`ringbuf.c` 未注册 → 无帧校验、无接收环。**❌ 复核:`HAL_Delay(1)` 仍在(`rs485.c:91`)**;`crc.c`/`ringbuf.c` 已删除(2026-10-02)→ 帧校验/接收环**从零重做**;`rs485_task.c` 仍未注册,消费者被 E-50(包撕裂)/E-49(TX 超时拉 DE)挡住 |
| `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/代码规范/`。
@@ -400,131 +342,140 @@ D-2/D-3 的共同根因:**没有任何机械检查**保证"文档说的 = 代
**设计良好**:`test/board/test_config.h` 的 `TEST_SUITE_*` 互斥(`#error` 保护)、CH395F 硬件层/NET 层 Phase 互斥、`@test` 一键闭环(烧录→wait-idle→标记校验)、模拟器 `--cfgtest/--logtest`。
> ⚠ 仍缺:**覆盖率概念**(原建议未办)、**注册检查**(`check_registration.py`)、**主机侧对 `lftpd`/`dhara` 移植层的测试**(N-12/N-13 这类缺陷至今只能靠人工读代码发现)。
**问题**:
| 编号 | 问题 | 建议 |
|------|------|------|
| 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` |
| P1-10 | CH395F 硬件测试套件 RAM 溢出(`L6406E`):HW 套 +8.7 KB、NET 层固件 +28 KB(`s_rx_buf` 32 KB) | 测试缓冲改为**分 Phase 复用同一块静态缓冲**(union 或分阶段 `memset`);`待处理内容.md` 的这条待办应升级为正式任务。**🟡 2026-10-11 复核:已升级为登记簿唯一 P 类收尾项**(RAM 侧因堆迁 CCM 大幅缓解,剩"CH395F 套件构建实测链接确认 + `@test` 闭环"两步) |
---
## 5. 内存优化(量化)
### 5.1 实测基线(map,2026-09-16)
### 5.1 实测基线(map)
| 指标 | 数值 | 上限 | 占用 |
|------|------|------|------|
| `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%** |
| 指标 | 2026-09-16(审查基线) | **2026-10-11 复核** | 上限 | 复核占用 |
|------|----------------------|--------------------|------|---------|
| `Code + RO-data` | 245,384 B(239.6 KB) | **273,860 B**(267.4 KB) | 1 MB Flash | **26.1%** |
| `ROM`(含 RW 初始化表) | 246,016 B | **274,060 B** | 1 MB | 26.1% |
| `RW-data` | 632 B | 824 B | — | — |
| `ZI-data` | 125,952 B | **139,896 B** | — | — |
| **SRAM1(0x20000000,128 KB)** | **126,584 B / 96.6%(余 4,488 B)** | **91,488 B** | 131,072 B | **69.8%(余 39,584 B)** |
| **CCM(0x10000000,64 KB)** | **0 B / 0%** | **49,232 B**(`RW_CCM` 49,184 + `UNINIT_CCM` 48) | 65,536 B | **75.1%(余 16,304 B)** |
| `Total RW (RW+ZI)` | 126,584 B | **140,720 B** | — | — |
`.sct` 只声明了 `ER_IROM1` + `RW_IRAM1 0x20000000 0x00020000 { .ANY (+RW +ZI) }`;文件头注释说明"未使用 SRAM3(克隆片可能没有)"——但**CCM 是 F407 片上必定存在的 64 KB**,与克隆风险无关(GD32F407 亦有 CCM)。
> **2026-10-11 二次实测(LVGL 死代码裁剪后,F-1/F-2/F-3 已落地)**:`Code=194,276 / RO=15,872 / RW=876 / ZI=139,892` → `ER_IROM1` **210,148 B(20.0%)**、`ROM 合计` **211,024 B(20.1%)**;`RW_IRAM1` 91,536 B(69.8%)与 `RW_CCM` 49,184 B(75.3%)**不变**(本次只动 Flash)。§5.4 的口径以此为准。
**RW+ZI 主要对象(实测 obj ZI/RW)**
**复核结论:R-2(启用 CCM)已部分落地**——`.sct` 现为手工 scatter,除 `ER_IROM1` + `RW_IRAM1 0x20000000 0x00020000` 外新增 `RW_CCM 0x10000000 0x0000FF00`(**只收 `heap_4.o (+RW +ZI)`**)与 `UNINIT_CCM 0x1000FF00 UNINIT 0x00000100`(`*(.noinit)`,崩溃现场跨复位保留区)。→ **FreeRTOS 堆 `ucHeap` 49,152 B 已迁 CCM**(.map `ucHeap 0x10000020`),任务栈从此不占 SRAM1,SRAM1 因此从 96.6% 降到 **69.8%**。⚠ **未迁的部分仍在 SRAM1**:LVGL 池 `work_mem_int` 40,960 B(`0x20008d00`)、UI 帧/绘制缓冲 `ui_port.o` 6,420 B、UI 网络发送 `ui_net_sink.o` 6,766 B——即 R-2 的"−54,122 B"只兑现了 49,184 B(堆),**LVGL 池与 UI 缓冲这 54 KB 里还剩 54,146 B 在 SRAM1**(见 §5.3 R-2 状态)。
| 对象 / 符号 | 字节 | 归属 | 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 | 网络 | 任意 |
> **基线口径说明**:上表取 **2026-10-11 00:38 链接的 `.map`(HEAD `54d6d2d`,干净树)**。复核期间工作区另有**未提交**改动(`adcTask` 栈 512→**640 words**、新增 `App/algo/adc_health.c/h` + 主机侧套件),其 01:12 重链接结果为 `Code=258,520 / RO=16,208 / RW=880 / ZI=139,896` → `Code+RO` **274,728 B(26.2%)**、`RW_IRAM1` **91,544 B(69.8%)**、`RW_CCM` 49,184 B(75.3%)。差异仅 +868 B Code / +56 B SRAM1,**不影响本节任何结论**;栈迁 CCM 后任务栈大小变化只影响堆内占用,不改 `ZI` 总量。
**两个静态池(49,184 + 40,960 = 90,144 B)就占了整个 128 KB 的 71%**——这是问题的核心。
**RW+ZI 主要对象(2026-10-11 复核,`scripts/ram_report.py` 排行;括号内为 2026-09-16 基线)**
**Flash 侧(Code+RO,214 个含码对象)**
| 对象 / 符号 | 字节 | 所在区域 | 归属 | DMA 可达性 |
|-------------|------|---------|------|-----------|
| `ucHeap`(`heap_4.o`) | 49,184(49,184) | **CCM**(`0x10000020`) | FreeRTOS 堆 | **DMA 不可达**——凡从堆里分配且被 DMA 直接寻址的缓冲都会失效(N-11 由此升级为**结构性约束**,见下) |
| `work_mem_int`(`lv_mem_core_builtin.o`,= `LV_MEM_SIZE 40*1024`) | 40,960(40,960) | **SRAM1**(`0x20008d00`) | LVGL 池 | 纯 CPU,**可迁 CCM(R-2 未完成项)** |
| `ui_net_sink.o`(`g_tx` + `g_lcdmem_data` + `g_rx`) | 6,766(6,762) | SRAM1 | UI 网络发送 | 可迁 CCM(CH395F 侧 memcpy 到自有 SRAM1 缓冲) |
| `ui_port.o`(`g_ui_fbuf` + `g_ui_draw_buf`) | 6,420(6,412) | SRAM1 | UI 帧/绘制缓冲 | 可迁 CCM |
| `nand_ftl.o`(3×2,048 页缓冲 + 控制块) | 6,216(6,216) | SRAM1 | 存储 | **必须 SRAM1**(N-11) |
| `lftpd.o` | 4,768 | SRAM1 | FTP | 任意 |
| `console_log.o`(行环形缓冲) | 4,120 | SRAM1 | 日志 | 任意(TX 走 DMA → 实际必须 SRAM1) |
| `ch395f.o`(`s_spi2_dma_tx/rx[2052]`) | 4,106(4,105) | SRAM1 | 网络 | **必须 SRAM1** |
| `default_task.o` | 2,728 | SRAM1 | 任务薄壳 | — |
| `gd5f2gq5ue.o`(`s_bbt_page_buf[2048]` + BBT 表) | ≈2,596(2,596) | SRAM1 | 存储 | **必须 SRAM1** |
| `sys_log_mcu.o` | 1,924 | SRAM1 | 日志 | 任意 |
| `cmsis_os2.o` | 1,724(1,724) | SRAM1 | RTOS 胶水 | 任意 |
| `sample_ring.o`(L2 输入 2 周波环缓冲,**新增**) | 1,448 | SRAM1 | 算法 | 任意 |
| `startup_...o`:`STACK 0x400`(`HEAP 0x200` 已被链接器移除) | 1,024(1,536) | SRAM1 | MSP + MicroLIB 堆 | 任意 |
| `tasks.o`(含 `pxReadyTasksLists[56]` = 1,120) | 1,280(1,280) | SRAM1 | RTOS | 任意(`configMAX_PRIORITIES` 仍 56 → **R-5 未办**) |
| 子系统 | 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% |
**两个静态池(49,184 + 40,960 = 90,144 B)仍是 RAM 问题的核心**,但**已分居两区**:堆在 CCM(占 CCM 的 75.3%)、LVGL 池在 SRAM1(占 SRAM1 的 45%)。→ 下一步收益点在**把 LVGL 池 + UI 两份缓冲(54,146 B)也搬进 CCM**,或按 R-9 先实测下调 `LV_MEM_SIZE`。⚠ 堆进 CCM 带来一条新铁律:**从 FreeRTOS 堆分配的缓冲不可被 DMA 直接寻址**(DMA2 不可达 `0x10000000`)——本仓已实测到同款约束(ADC1 DMA 目的缓冲必须在 `RW_IRAM1`),应写入 `docs/内存管理/`。
### 5.2 结构性诊断(四条)
**Flash 侧(Code+RO)**:基线为 214 个含码对象;**2026-10-11 复核为 182 个含码对象**(按 `.map` 组件表对象名归类,`App/`+`Src/`+`Drivers/BSP/` 合并为"应用/驱动自研",算法层单列)
| 子系统 | Code+RO(基线) | 占镜像 | **2026-10-11** | **占镜像** |
|--------|----------------|--------|----------------|-----------|
| LVGL(含核心/绘制/控件/字体/缓存/图像) | ≈150,600 | **61.5%** | **152,185** | **55.4%** |
| 应用/驱动自研(`App/` + `Src/` + `Drivers/BSP/`,不含 UI 页面) | 10,468(仅 App+Src) | 4.3% | **27,076** | 9.9% |
| 网络(NET + NETCFG + lftpd) | 18,245 | 7.4% | **21,629** | 7.9% |
| 手写 UI 页面/框架(`ui_*.o`,现 6,036 行) | 17,520 | 7.2% | **18,535** | 6.8% |
| 存储(FatFs + dhara + NAND) | 8,222 | 3.4% | **15,133** | 5.5% |
| HAL/CMSIS | 11,866 | 4.8% | **14,391** | 5.2% |
| FreeRTOS + CMSIS-RTOS | 8,011 | 3.3% | **12,258** | 4.5% |
| 算法层 L1/L2(`adc_input`/`phasor`/`sample_ring`,**新增**) | — | — | **1,589** | 0.6% |
| 其他(startup/CMSIS 胶水等) | — | — | 5,842 | 2.1% |
| **合计(含库成员与填充)** | **245,384** | 100% | **274,728** | 100% |
**复核要点**:LVGL 的**绝对量几乎没动**(150,600 → 152,185),占比从 61.5% 降到 55.4% 纯粹是因为**自研侧涨了 ≈30 KB**(看门狗 + Fault 采集 + ADC 全链 + 相量层 + 日志/观测)。→ **§5.4 的裁剪结论依旧成立且更划算**:省下的 54.6 KB 现在相当于镜像的 **19.9%**。
### 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 已是实例)。
→ **未闭环**:LVGL 池 **40,960 B** + UI 两份缓冲 **13,186 B** 仍在 SRAM1;堆迁入后 **CCM 只剩 16,304 B,装不下这 54 KB** → R-2 的剩余部分**必须先做 R-9(下调 `LV_MEM_SIZE`)或 R-4(帧副本去重)**才有余地。
2. **任务栈全部动态取自 FreeRTOS 堆**:`osThreadAttr_t` 只设 `stack_size` 不设 `stack_mem`(`Src/freertos.c:64-105`)→ 栈 + idle/timer 栈 + TCB + IPC 全部从堆里扣,**留给动态分配的实际余量很紧**。
→ **未闭环**:7 条任务(`defaultTask/netTask/ftpTask/testTask/uiTask/timeSyncTask/adcTask`)栈合计 **10,176 words = 40,704 B**,仍全部动态取自堆。实测 `load: OK (523B, heap free 8840B)` → 空闲约 **8.8 KB / 49,152 B**,历史最低水位(`xPortGetMinimumEverFreeHeapSize`)**6,640 B**(2026-10-11 L2 相量 + L1-6 上链后由 7,152 B 降下来);同日 `adcTask` 栈由 512 抬到 **640 words**(静态最坏链 1,080 B),继续蚕食堆余量。**R-6(栈静态化)是把"池内碎片"变"可核算静态区"的正解,且其前置条件(观测常开)已满足。**
3. **零观测**:`xPortGetFreeHeapSize`/`xPortGetMinimumEverFreeHeapSize`/`uxTaskGetStackHighWaterMark` 全库零调用,无法回答"还能加多少"。
→ 已解决(三面观测:`[STACK]` 运行时水位 + `scripts/stack_report.py` 静态最坏链 + `ram_report.py`/`check_mem_numbers.py` 门禁),不再列为待办。
4. **MicroLIB 512 B C 堆是隐形地雷**:任何新代码写一个 `malloc` 都会中招。
→ **未闭环**:`Heap_Size EQU 0x200` 未变、`.map` 仍 `Removing startup_stm32f407xx.o(HEAP), (512 bytes)`;铁律已进 AGENTS.md(只用 `pvPortMalloc/vPortFree` 或收进平台后端),但**没有机械化检查**——`check_coding_standard.py` 未含 `malloc(` 正则 → §5.5 第 5 条未落地。
### 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 |
| 编号 | 措施 | 收益 | 风险 | 优先级 | **2026-10-11 状态** |
|------|------|------|------|--------|---------------------|
| **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,本季度** | 🟡 **只做一半**:`.sct` 加了 `RW_CCM`,但**只收 `heap_4.o`**(49,184 B)→ SRAM1 −49,184 B。LVGL 池 + UI 两份缓冲(54,146 B)**未迁**;`APP_CCM_ENABLE` 宏**未采用**(该宏已从 `app_cfg.h` 删除)。⚠ 复核新增前置条件:**CCM 只剩 16,304 B,这 54 KB 装不下** → 必须先做 R-9 或 R-4 |
| **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 | ❌ 未办(合并未做);🟡 但 `s_bbt_page_buf` 已由 2,048 B 收紧为 **272 B**(2026-09-21,等效省 1,776 B) |
| **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 | ❌ 未办(`ui_port.o` 6,420 + `ui_net_sink.o` 6,766 = 13,186 B,副本数未变) |
| **R-5** | `configMAX_PRIORITIES` 56 → 32(实际最高用到 `osPriorityHigh` 级别) | **−480 B** | 低 | P2 | ❌ 未办(`FreeRTOSConfig.h:69` 仍 56;`pxReadyTasksLists[56]` = 1,120 B 仍在) |
| **R-6** | 任务栈改**静态分配**(`stack_mem` 指向各自 `.bss` 数组),从堆里释放 32,768 B | 堆可用 +32 KB(**总量不变**,但把"池内碎片"变成"可核算静态区") | 低-中:静态后溢出检测更难发现;先测水位再定尺寸 | P2 | ❌ 未办(仍全部 `stack_size` 动态)。⚠ 前置条件已满足:观测已三面常开,`stack_report.py` 已给出每任务最坏链 → 可直接按实测值定尺寸 |
| **R-8** | 观测常开:新增 `memStatsTask`(最低优先级,1 Hz,512 w)打印 `xPortGetFreeHeapSize` / `xPortGetMinimumEverFreeHeapSize` / 各任务高水位 / `lv_mem_monitor`,并支持串口命令按需打印 | 0(可核算) | 低 | **P1**(是 R-2/R-6 的前置) | 🟡 **目标达成、形态不同**:未建 `memStatsTask`,改为 ① 开机/自检打印 `[STACK]` 水位 + 堆空闲;② `scripts/stack_report.py`(静态最坏链)+ `ram_report.py`;③ `check_mem_numbers.py` 门禁 + `docs/内存管理/data/` 快照。**已足够支撑 R-2/R-6 决策** |
| R-9 | `LV_MEM_SIZE` 40 KB → 依 R-8 实测峰值下调(既有方案预估 16 KB) | −16 ~ −24 KB | 低(有 R-8 兜底) | P2 | ❌ 未办(`lv_conf.h:44` 仍 `(40 * 1024)`)。⚠ 复核:`simulator/tests/check_content_pages.py` 已带**池水位门禁** → 实测峰值数据已具备,本条只差改数值 + 真机复跑(⚠ 两端 `lv_conf.h` 要同步改) |
**组合结果预估(注意措施之间存在重叠,不可简单相加)**
| 组合 | 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 |
| **+ 实际执行(2026-10-11 实测)** | **91,488 B / 69.8%** | 余 **39,584 B**。实际做的 ≠ 预估路径:**R-2 只搬堆(−49,184 B → CCM 49,232 B / 75.1%)** + R-7(−8,192 B)+ BBT 缓冲收紧(−1,776 B);抵消项 = adcTask 栈、`sample_ring` 1,448 B、`console_log` 4,120 B、Fault/看门狗/日志新增 |
| + R-2(CCM 启用,原预估全量) | **72,462 B / 55.3%** | −54,122 B;CCM 用 54,122 B(仍余 11 KB)。⚠ 复核:该预估**已不可直接执行**——堆先占了 CCM 49,184 B,CCM 只剩 16,304 B,装不下 LVGL 池 + UI 缓冲这 54 KB |
| + R-3(存储页缓冲合并) | **≈66.3 KB / 50.6%** | −2,048 ~ −4,096 B(BBT 收紧已等效省 1,776 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` 注册)预留出预算空间。
最终形态(原预估):**SRAM1 约 50%、CCM 约 20% 使用率**,`L6406E`(P1-10 测试镜像溢出)随之消解,并为 ADC 数据路径与 RS485 协议栈预留预算空间。
> 与 `docs/内存管理/RAM使用分析与优化方案.md` 的关系:本表**沿用其阶段划分**(阶段 0 测量 / 阶段 1.1 池收缩 / 阶段 1.2 CCM / 阶段 2 堆收缩),新增的是 ①R-4 帧副本去重(原文档未识别 4 份副本)②R-3 存储侧缓冲合并 ③N-11 的 **DMA 可达性硬边界**(原文档未提出,属实施风险关键点)④R-1/R-8 的观测前置条件。
> **2026-10-11 修正(未闭环部分)**:原预估的**执行顺序已不可按原样走**——实际走到 **SRAM1 69.8% / CCM 75.3%**,因为选了"先搬堆"而非"先搬 LVGL 池 + UI 缓冲",**CCM 已被堆占掉 75%(只剩 16,304 B)**。要拿到剩余收益,顺序必须改为 **R-9(下调 `LV_MEM_SIZE`,最省事的杠杆)或 R-4(帧副本去重)→ 再做 R-2 后半段**。另两条仍开放:**`L6406E` 未完全消解**(登记簿留 P1-10 收尾:CH395F 套件构建实测链接确认 + `@test` 闭环);RS485 协议栈前置件 `crc.c`/`ringbuf.c` 已删除,**需从零重做**而非"注册"。
### 5.4 Flash 优化(≈50 KB 可证明死代码)
> 与 `docs/内存管理/RAM使用分析与优化方案.md` 的关系:本表**沿用其阶段划分**(阶段 0 测量 / 阶段 1.1 池收缩 / 阶段 1.2 CCM / 阶段 2 堆收缩),新增的是 ①R-4 帧副本去重(原文档未识别 4 份副本)②R-3 存储侧缓冲合并 ③N-11 的 **DMA 可达性硬边界**(原文档未提出,属实施风险关键点)④观测前置条件(已落地,见 §5.5)。
**根因:`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` 自绘。
### 5.4 Flash 优化(未闭环:F-4 / F-5 / F-6)
| 编号 | 措施 | 可省 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` 浮点裁剪选项 |
> **F-1 / F-2 / F-3 已落地并销项(2026-10-11)**:`Lib/lv_conf.h` 与 `simulator/lv_conf.h` 对齐为同一特性子集(只留 `LV_DRAW_SW_SUPPORT_I1`、零控件、`LV_USE_THEME_SIMPLE 0`、`LV_USE_FLEX/GRID 0`)→ **实测省 64,576 B**:`ER_IROM1` 274,724 → **210,148 B**,Flash 26.3% → **20.1%**。比本报告原估(54,630 B)多省约 10 KB——锚点消失后,`blend_to_i1` 内部按**源**格式的分支与 `transform` 的格式分支也一并退出。完整记录(对象级对照、上游 9.3.0 漏守卫的配套补丁、ui-verify 证据)见 `../内存管理/RAM使用分析与优化方案.md` §2.6.4 与 `../UI设计/UI_设计架构.md` §1.4 第 10 条。
**合计可回收 ≈ 50–60 KB Flash(镜像的 20%+)**,且**不减少任何现有功能**。这是"低风险高收益"的一类改动,建议与 R-1 同批做(先 Flash 后 RAM,避免同时改两处难定位)。
| 编号 | 措施 | 可省 Flash | 依据 | **2026-10-11 状态** |
|------|------|-----------|------|---------------------|
| **F-4** | 关闭未用的绘制子模块与 `LV_USE_SNAPSHOT`(MCU 端)/`LV_USE_OBJ_PROPERTY`/`LV_USE_FLOAT` 等 | 可评估部分 **≈10 KB** | 新镜像对象实测:`mask` 3,862 + `box_shadow` 3,246 + `draw_sw_img` 2,960 + `bin_decoder` 1,972 + `image_decoder` 952 + `triangle` 930 + `transform` 598 + `grad` 420 + `mask_rect` 372 = **15,312 B**;工程无图片、无阴影、无变换 | ❌ 未办(数字已按 F-1 后的新镜像重测,`transform` 已从 4,340 降到 598)。⚠ **不可盲目关**:`mask`/`mask_rect`/`grad` 是文字与矩形绘制的必经路径;真正可评估的是 `box_shadow` + `draw_sw_img` + `bin_decoder` + `image_decoder` + `triangle` ≈ **10.1 KB** |
| **F-5** | 从 uvprojx 中**移除未使用的 LVGL 控件源文件**(201 个 → 实际需要的子集) | 编译时长 −40% 左右 | 与 F-2 配合;`lv_conf` 关开关后文件仍参与编译,删文件才省编译时间 | ❌ 未办(uvprojx 现挂 **306 个 `.c`,其中 LVGL 仍 201 个**)。⚠ F-2 落地后这些 TU 现在编出来是**空目标文件**(文件级 `#if` 全灭),删文件的收益更纯粹、风险更低 |
| **F-6** | 双精度软浮点:`dadd`/`dmul`/`ddiv`/`depilogue`/`dfixul`(`mf_w.l`),来自 `printf` 的 `%f`/double 提升 | −1 KB + 运行期性能 | 日志与 UI 全用定点;把 `%f` 改整数刻度 | ❌ 未办(新镜像仍链入 `dadd.o`) |
### 5.5 内存治理长效机制
**剩余可回收 ≈ 10–11 KB Flash**(F-4 可评估部分 + F-6);F-5 省的是编译时长不是 Flash。原"≈50 KB 死代码"的主体(64,576 B)已在 2026-10-11 消除。
### 5.5 内存治理长效机制(+ 2026-10-11 复核)
1. **`scripts/ram_report.py` 升级为门禁**:超阈值(如 SRAM1 > 92%)即失败,并在 PR 描述里自动附 Top-10 对象表。
→ **🟡 部分实现**:`scripts/check_mem_numbers.py` 已挂 pre-commit(以 `.map` 为**唯一事实源**,校验 `docs/内存管理/RAM使用分析与优化方案.md` 的 `MEM_SNAPSHOT:BEGIN/END` 受管区 + 散句陈旧数字扫描)——**防的是"文档数字漂移",不是"占用超阈值"**。⚠ 无 92% 阈值失败逻辑;且 CI 上无 `.map` 时该钩子自适应跳过(登记簿 **A-5 残余**)。
2. **建立 `[计划] scripts/ram_baseline.tsv`**(`docs/内存管理/RAM使用分析与优化方案.md:172` 已承诺但未创建):每次构建记录 Code/RO/RW/ZI/Top 对象,形成趋势。
→ **❌ 未建**;🟡 等效物已存在:`docs/内存管理/data/` 下 5 份 `ram_report`/`stack_report` 快照留档(人工触发,非 tsv 趋势表)。
3. **RAM 预算台账**(同上,文档已承诺 `[计划] docs/内存管理/RAM预算台账.md`):按子系统配额(网络 12 KB / 存储 12 KB / UI 16 KB / RTOS 48 KB / 应用 8 KB),超配额必须先申请。
→ **❌ 未建**。⚠ 复核:这条的价值在上升——堆已进 CCM 且只剩 16,304 B CCM 余量,"谁还能用 CCM"必须有台账,否则 R-2 后半段无法排期。
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(` 即告警)。
→ **🟡 半落地**:独立清单文档未建,但**铁律已进 AGENTS.md**("DMA 目的缓冲必须在 SRAM,DMA2 不可达 CCM 0x10000000"、"改 ADC 流后回 NVIC 核对");代码侧另有**运行时**守卫(`gd5f2gq5ue.c:110-136` 不可达回落轮询)。⚠ 仍缺**编译期/启动期断言**,且需补一条新铁律:**从 FreeRTOS 堆分配的缓冲不可被 DMA 直接寻址**(堆已在 CCM)。
5. **禁止新增 C 库 `malloc`**:写进 `docs/代码规范/`,并在 `check_coding_standard.py` 加正则检查(`App/`、`Drivers/` 内出现 `malloc(` 即告警)。
→ **🟡 前半已做、后半未做**:AGENTS.md 已有"自研代码禁用 C 库堆分配"铁律(含模拟器照不出来的原因);❌ `check_coding_standard.py` **未含 `malloc(` 正则** → 目前只靠人评审,属机械检查缺口。
---
@@ -534,14 +485,9 @@ D-2/D-3 的共同根因:**没有任何机械检查**保证"文档说的 = 代
| 层 | 现状 | 评价 |
|----|------|------|
| 声明源 | `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/`(**不存在**) | ❌ 无视觉基线 |
| 内容页 | `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 映射**。**2026-10-11 复核:未改善**——`ui_menu_tab.h:28` 仍 42 节点(**叶子 33 个**),有内容页的叶子 **6 个**(PASSWORD 非叶子映射);`menu_content_page_for()` 现位于 `ui_menu.c:118-128`,仍是手写 `case 1u/3u/4u/30u/39u/40u:` |
| 设计令牌 | **无**(每页自带 `CP_SCREEN_W/H 160`、`CP_TITLE_Y 2`、`CP_PAGE_LINE_Y 143`、`CP_ROW_BUF_LEN 32` 等布局常量) | ❌ 魔法数字分散在各页。**2026-10-11 复核:`Drivers/BSP/UI/ui_tokens.h` 仍不存在**(U1 未办) |
| golden 回归 | 已建成,但**不在文档承诺的 `tools/ui_mock/`**,而是 `simulator/tests/` 四脚本:`check_menu_rows.py`(菜单 8 状态全像素)、`check_content_pages.py`(内容页黄金参考逐字节 + 文本断言 + LVGL 池水位门禁)、`regen_ref_png.py`、`check_remo_sink.py` | 🟡 **有基线但未入门禁**:两个脚本未挂 pre-commit / `ui-verify`;`docs/UI设计/UI_界面制作流程.md` / `UI_自动化测试流程.md` 仍指向不存在的 `tools/ui_mock/`(语义断链) |
### 6.2 新增一个页面要走多少步(当前)
@@ -549,6 +495,8 @@ D-2/D-3 的共同根因:**没有任何机械检查**保证"文档说的 = 代
**5 步是纯机械劳动且 2 处易错(步骤 5 的魔法下标、步骤 6/7 漏注册)。**
> **2026-10-11 复核:8 步流程一步未减。** 步骤 5 的手写 `case` 仍在(`ui_menu.c:118-128`);步骤 6/7 的漏注册仍无机械校验(`check_registration.py` 未建)——但已新增一条**间接暴露路径**:若新文件落在新目录而没把目录加进 `uvprojx` 的 `<IncludePath>`,消费者 TU 会报 `cannot open source input file`(AGENTS.md 2026-10-10 记录),这类报错落在**别的文件**上、易误判,仍不等于"存在但未注册"的检查。
### 6.3 标准化目标:四层 + 一个闭环
```mermaid
@@ -608,6 +556,7 @@ extern const lv_style_const_prop_t UI_STYLE_FLAT[];
| `action`(确认型) | 强制复归、清除记录、手动录波 | 提示 → 二次确认 → 执行 → 结果日志 |
菜单树 42 个叶子中,**除已实现的 7 个外,其余 35 个可 100% 归入这 4 类原型**(form 21 个、records 9 个、detail 3 个、action 2 个)。这意味着**剩余页面工作量从"35 页 × 各自设计"降为"4 个原型 + 35 份数据绑定"**。
> **2026-10-11 口径修正**:42 是 `ui_menu_tab.h:28 UI_MENU_TAB_COUNT`(根 + 41 节点)的**节点数**,**叶子实为 33 个**;已有内容页的叶子 **6 个** → 待落地 **27 页**(不是 35)。四类原型的分布按 27 页重算即可,结论不变。
**L3 页面实例** — 目标形态(以"遥测死区"为例,全部新代码约 30 行):
@@ -629,10 +578,11 @@ const ui_page_desc_t ui_page_deadzone = UI_FORM_PAGE("遥测死区", s_items_dea
产物:`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` 从"记得跑"变成"漏跑就红")。
**闭环门禁(1 天)**——未闭环 2.5 条:
1. `[计划] scripts/check_registration.py`(同 §2.3 S-3)保证新页面两端都注册 → **❌ 仍未建**(`scripts/` 现有 11 个脚本,无此项)。
2. **golden 回归**(原方案:`simulator --shot` 导出 1bpp 帧 + PNG 逐像素比对)已按另一路径建成(`simulator/tests/` 四脚本,见 §6.1)。**未闭环**:`UI_界面制作流程.md:413` 与 `UI_自动化测试流程.md:104` 仍指向不存在的 `tools/ui_mock/`;这些脚本未挂进 pre-commit / `ui-verify` 门禁。
3. 字符集自动校验:构建前扫描所有 `ui_*.c` 中的汉字,与 `ui_cn_chars.txt` 求差集,缺失即 FAIL(把 `font-regen` 从"记得跑"变成"漏跑就红") → **❌ 未入门禁**:`font-regen` skill 已把四步固化并写进 AGENTS.md("含新汉字的 UI 文案提交前先跑"),但**仍是约定不是门禁**(无 pre-commit 钩子做差集检查)。
### 6.4 UI 侧的内存纪律(必须写进规范)
@@ -640,19 +590,19 @@ const ui_page_desc_t ui_page_deadzone = UI_FORM_PAGE("遥测死区", s_items_dea
|------|------|
| 页面**不得**新增静态大缓冲;文本缓冲统一用 `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% |
| 优先自绘(`lv_draw_*` + `lv_text_get_width`),新控件需评审(会破坏 §5.4 F-2 **已落地**的裁剪,且须两端同步) | LVGL 现占镜像 **41.6%**(87,706 / 210,623 B;裁剪前 55%) |
| 帧缓冲任何改动必须遵守 R-4(副本数 ≤ 2) | 当前 4 副本 12.8 KB |
| `LVGL 池`用量必须经 `lv_mem_monitor` 实测后才允许扩大 | 现 40 KB,预估可降到 16 KB |
### 6.5 实施顺序(UI 专项,总计约 8 人日)
### 6.5 实施顺序(UI 专项,总计约 8 人日;2026-10-11 复核)
| 步骤 | 内容 | 交付判据 |
|------|------|---------|
| 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` 哨兵命中 |
| 步骤 | 内容 | 交付判据 | **2026-10-11 状态** |
|------|------|---------|---------------------|
| U1 | `ui_tokens.h` 令牌化(纯替换,不改行为) | `@build` PASS + 模拟器截图与替换前逐像素一致 | ❌ 未办(`ui_tokens.h` 不存在)。🟡 **但它的验收条件如今已可机械执行**——golden 基线已建,"逐像素一致"从口头判据变成可跑断言 |
| U2 | `ui_w_records`(合并 runlog/selftest) | 两页行为不变,代码 −1 个文件 | ❌ 未办(`ui_page_runlog.c` / `ui_page_selftest.c` 仍是两份结构几乎相同的代码) |
| U3 | `ui_w_form` + `ui_page_desc_t` 原型化 + 生成器产注册表 | 手工 `menu_content_page_for()` 删除;新增一页只需"改树 + 加实例文件" | ❌ 未办(`ui_page_registry.c` / `gen_page_registry.py` 均不存在;手写 `case` 仍在) |
| U4 | golden 回归(模拟器 `--shot` + PNG 比对)+ 字符集检查 + 注册检查入 pre-commit | 三个新 hook 全绿 | 🟡 **1/3 落地**:golden 已建(`simulator/tests/` 4 脚本,见 §6.1);❌ 字符集差集检查未入门禁;❌ 注册检查未入门禁 |
| U5 | 用原型批量落剩余 35 页(每页 ≤30 行) | 菜单树全绿、无 `CONTENT_PAGE_COUNT` 哨兵命中 | ❌ 未动。**复核修正口径**:42 节点 / **33 叶子** / 6 个叶子有内容页 → 待做 **27 页**(不是 35) |
---
@@ -660,91 +610,63 @@ const ui_page_desc_t ui_page_deadzone = UI_FORM_PAGE("遥测死区", s_items_dea
### 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` 零告警 |
| 编号 | 事项 | 验收 | **2026-10-11 状态** |
|------|------|------|---------------------|
| N-02 | FTP 鉴权(或生产构建默认关闭 FTP 写) | 错误口令登录被 `530` 拒绝;`@test` 加 FTP 标记校验 | ❌ **未办,且未登记进登记簿** → 复核建议:补登为 P1(现场可被任意主机改写 IP/MAC 并断网) |
| N-06 | 5 个未注册 `.c`:注册或显式删除 + 白名单 | `check_registration.py` 零告警 | 🟡 **半**:3 个消解(2 删 1 注册);❌ `rs485_task.c`/`lftpd_log.c` 仍未处理,❌ 脚本与白名单未建 |
### 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 轮询)~~ **已落地(2026-10-10,改走"门控"而非事件驱动)** | ✅ 无客户端时队列占用 **100 msg/s → 0**;实测 `uis` 50 ms → **1 µs**(`../UI设计/RemoDispBus协议与接收设计说明.md` §3;判读见 `../子系统设计/看门狗设计说明.md` §11.4) |
| 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 溢出) | 台账闭环 |
| 编号 | 事项 | 预期 | **2026-10-11 状态** |
|------|------|------|---------------------|
| N-04 / R-2 | **CCM 启用**(LVGL 池 + UI 帧缓冲,`APP_CCM_ENABLE` 门控)+ N-11 DMA 边界文档与断言 | SRAM1 96.6% → ~55% | 🟡 **半**:堆 49,184 B 已进 CCM → SRAM1 **69.8%**;LVGL 池 + UI 缓冲未迁(CCM 只剩 16,304 B,**必须先做 R-9/R-4**);`APP_CCM_ENABLE` 未采用;N-11 侧改为**运行时守卫**,仍无编译期断言 |
| R-4 | UI 帧副本 4 → 2 | −6.4 KB | ❌ 未办(`ui_port.o` 6,420 + `ui_net_sink.o` 6,766 未变) |
| N-12/N-13 | `dhara_nand_is_free` 全页校验;`dhara_nand_copy` 处理 `DHARA_NO_PAGE` | 掉电注入测试通过 | ❌ 未办(两处代码原样) |
| N-16 | 日志常驻 `FIL` + 批量 flush;UI 记录读缓存升级 | 单条日志 NAND 操作次数下降 ≥50% | ❌ 未办(每条仍 open/write/close) |
| D-1 | 芯片手册图片降采样 / 拆 submodule | clone 40 MB → ~8 MB | ❌ 未办;⚠ 且 `.git` 已膨胀到 **149 MB** → 还需一次历史重写,优先级上升 |
| U1–U4 | UI 令牌化 + 原型库 + 生成器 + golden | 新增一页 ≤30 行 | 🟡 **U4 的 golden 已建**(`simulator/tests/` 4 脚本);U1/U2/U3 未动;U4 的注册检查与字符集检查未入门禁 |
| P1-7 / P1-8 / P1-10 | 沿用台账(MAC 写序 / 重建失败恢复 / 测试 RAM 溢出) | 台账闭环 | P1-7/P1-8 本轮未复核,仍按台账跟踪;**P1-10 🟡 收尾中**(登记簿唯一 P 类项,剩"套件构建实测链接确认 + `@test` 闭环"两步) |
### 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 最小集)。
`R-3/R-5/R-6/R-9`、`F-4/F-5/F-6`、S-1/S-2/S-5、P2-1..P2-16 余项。
> **2026-10-11 复核**:`R-3/R-5/R-6/R-9`、`F-4/F-5/F-6`、**S-1/S-2** ❌ 未办(`NETCFG`/`UI` 的语义层错位、4 处裸 `extern` 原样);**S-5 🟡 已实现核心**(`App/fs/vol_lock.c` + 主机侧 33 例单测),"可选全序列卷锁"的剩余范围在登记簿。
### P3(机会性)
UI 剩余页面批量落地(U5)、目录级层次迁移(S-1 的物理搬迁)、`app_cfg.h` 开关体系重构、`Lib/lv_conf.h` 迁至 `Config/`。
> **2026-10-11 复核**:四条全部未动。⚠ 优先级更新:`Lib/lv_conf.h` 迁至 `Config/` 原建议"与 F-1..F-3 合并做"——**F-1..F-3 已于同日落地但位置未迁**,现需单独做,且要一并处理 `simulator/lv_conf.h`(两份必须保持同一特性子集,见 `../UI设计/UI_设计架构.md` §1.4 第 10 条);U5 的口径改为 **27 页**(33 叶子 − 6 已实现)。
### 交付物清单(可直接排期)
| 交付物 | 类型 | 来源条目 |
|--------|------|---------|
| `[计划] 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 |
| 交付物 | 类型 | 来源条目 | **2026-10-11 状态** |
|--------|------|---------|---------------------|
| `[计划] scripts/check_registration.py` | 新脚本 + pre-commit hook | S-3 / N-06 | ❌ **仍未建**(`scripts/` 11 个脚本里没有它)——这是本报告里**唯一一条"1 小时工作量、至今未做"**的机械检查 |
| `[计划] scripts/ram_baseline.tsv` + `[计划] docs/内存管理/RAM预算台账.md` | 文档承诺落地 | §5.5 | ❌ **未建**;🟡 等效物 = `docs/内存管理/data/` 5 份快照 + `MEM_SNAPSHOT` 受管区(无趋势、无配额) |
| `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 | ❌ 未建(`gen_menu.py` 未扩展,注册表不存在) |
| `[计划] docs/内存管理/DMA可达性清单.md` | 规范(含"哪些缓冲禁止进 CCM") | N-11 | 🟡 **半**:独立清单未建,但铁律已进 AGENTS.md + 代码有运行时守卫;⚠ 需补"堆缓冲不可 DMA"这一新条 |
---
## 附录 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/` | 改路径 |
| 结论 | 证据 | **2026-10-11 复核** |
|------|------|---------------------|
| SRAM1 96.6%、余 4,488 B | map:`Total RW Size (RW Data + ZI Data) 126584 (123.62kB)`;scatter `RW_IRAM1 0x20000000 0x00020000` | 🟡 已缓解:`RW_IRAM1` **91,488 / 131,072 = 69.8%(余 39,584 B)**;`Total RW` 140,720 B |
| 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` | ✅ 结论不变:`Heap_Size EQU 0x200` 未改,map 现 `Removing startup_stm32f407xx.o(HEAP), (512 bytes)`(未被引用 → 整段移除) |
| lftpd 已迁 FreeRTOS 堆 | map `lftpd_io.o refers to heap_4.o(i.pvPortMalloc)` | 不变 |
| 任务栈全部动态分配 | `Src/freertos.c:64-105` 的 `osThreadAttr_t` 无 `.stack_mem`;`freertos.o` ZI 仅 1,104 B | 不变(R-6 未办);7 任务栈合计 10,176 words,其中 adcTask 已抬到 640 words |
| NAND 直接 DMA 调用方缓冲 | `gd5f2gq5ue.c:200`、`:258`(`GD5F_DMA_THRESHOLD = 32`) | 🟡 模式未变(`:325` 等),阈值改 **1024**(`gd5f2gq5ue.h:78`)+ 新增**运行时 DMA 可达性守卫**(`:110-136`,回落轮询) |
| CH395F memcpy 自有缓冲 | `ch395f.c:1132-1190` | 不变(UI 缓冲仍可迁 CCM 的前提依旧成立) |
| FTP 无鉴权 | `Drivers/BSP/NET/lftpd/lftpd.c:367-371` | ❌ 仍在:现 `lftpd.c:375-379`(`cmd_pass`)/ `:553-557`(`cmd_user`) |
| 端口分配 | `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_tab.h:28 UI_MENU_TAB_COUNT 42`(根 + 41 节点)→ **叶子 33**、**有内容页的叶子 6**(枚举 7 页含 PASSWORD) |
| 手写映射 | `ui_menu.c:155/:205 menu_content_page_for()` | 仍在,现位于 `ui_menu.c:118-128`(手写 `case 1u/3u/4u/30u/39u/40u:`) |
| 文档体量 | 工作树字节统计(见 §3.1 表) | 见 §3.1 复核列:受控 2,363 文件 / 77.7 MB,`docs/` 849 / 44.4 MB,图片 703 张 / 36.3 MB,`.git` **149.3 MB** |
## 附录 C 评审中确认"无需改动"的设计(避免后续误改)
@@ -760,4 +682,42 @@ UI 剩余页面批量落地(U5)、目录级层次迁移(S-1 的物理搬
---
**报告结束。** 建议把 §7 的 P0 六条直接追加进 `docs/待处理内容.md`(沿用其编号体系,本报告编号 `N-01…N-18` 作为交叉引用),并把 §5/§6 的落地项写进 `[计划] docs/内存管理/RAM预算台账.md` 与 `[计划] docs/文档规范.md`。
**报告结束。**
> **2026-10-11 复核收尾**:
> - ⚠ **台账漏登两条**:本报告 **N-02(FTP 无鉴权)** 与 **N-12/N-13(dhara 移植缺陷)** 既未修复也未登记在 `../待处理内容.md` → 建议补登。
> - ⚠ **编号撞车**:本报告曾用 N-18 指"无 CI"(该条已销项删除),而登记簿的 N-18 = "8 路电气量分属两片转换器(V0.2 改版项)"。引用时一律带文档名。
> - `[计划] docs/内存管理/RAM预算台账.md` 与 `scripts/ram_baseline.tsv` **仍未创建**(见 §5.5)。
> - 本报告按 `docs/README.md` 的定位是**体系层评审报告,不是待办池**——待办一律以 `../待处理内容.md` 为准;本报告只保留**未闭环问题的发现 + 证据**,修复即销项。
---
## 变更记录
| 日期 | 类型 | 说明 |
|------|------|------|
| 2026-09-18 | 初版 | 全量审查;基线 `HEAD = 2d0ec35`、`.map` 链接于 2026-09-16 12:33;产出 N-01..N-18 / D-1..D-7 / S-1..S-5 / R-1..R-9 / F-1..F-6 / U1..U5 编号体系 |
| 2026-09-18 | 实施记录 | N-01 按方案 A 落地(两处必要调整:分配收进 `cfg_file_store` 后端、`_Static_assert` 改 typedef 负数组) |
| 2026-09-25 | 归档 | §3 文档分布章节标注"已归档(章节保留作历史)" |
| 2026-10-10 | 就地更新 | N-10 改走"门控"并销项 |
| 2026-10-11 | 现状复核 | 按当前代码与 `.map` 重跑全部数字并就地标注(§0.1 评级、§1.3 口径、§2.1 规模、§3.1 体量、§4.x 逐条、§5.1 基线双列 + 对象排行、§5.3/§5.4 状态列、§6/§7 状态);复核基线 `HEAD = 54d6d2d` + 00:38 `.map` |
| **2026-10-11** | **修复即销项(本次)** | 与登记簿同一规则:**已修复项的分析正文与表格行从本报告删除**,报告只呈现未闭环问题(**874 → 733 行**,114 KB → 84 KB)。删除范围:§4.4.1 N-01 方案与实施记录、§4.1 N-03a/b·N-07·N-08·P2-13、§4.2 N-10、§4.8 P2-14/P2-15/N-17/N-18、§3.3 D-2..D-7、§5.3 R-1/R-7、§6.1 已闭环四层与字体、§7 P0/P1 已办行、交付物清单已建项、附录 A 已修行、**附录 B 断链清单整节**。§0.2/§0.4 改写为**未闭环清单**。原始推理与取证在未闭环条目中一律保留 |
| **2026-10-11** | **实施 + 销项:F-1/F-2/F-3** | `Lib/lv_conf.h` 与 `simulator/lv_conf.h` 对齐为同一特性子集(只留 `LV_DRAW_SW_SUPPORT_I1`、5 个未用控件类全关、`LV_USE_THEME_SIMPLE 0`、`LV_USE_FLEX/GRID 0`,并删掉 9.3 不存在的 `LV_USE_THEME_BASIC` 与无效 `LV_USE_OBJ`);配套补 `lv_draw_sw_blend_to_i1.c` 上游漏的守卫 → **省 64,576 B**:`ER_IROM1` 274,724 → **210,148 B**,Flash 26.3% → **20.1%**(比原估 54,630 B 多约 10 KB)。验证:`@build` 0 错 0 警;ui-verify 五步全过(**11 页黄金参考在裁剪前生成、裁剪后逐字节一致**,菜单 8 状态像素一致,池水位 5440/5520 → 5368/5448);`@test` 真机 PASS。本报告同步:§5.4 只留 F-4/F-5/F-6(数字按新镜像重测)、§0.1 Flash 评级 B→A−、§0.2 Top 6→Top 5、§0.4 Flash 行、§5.1 加二次实测、附录 A 删两条已闭环证据行 |
### 已销项条目的记录去处
> 删除前已逐条核对:以下去处文档均含完整记录,删了不丢信息。
| 已销项条目 | 去处 |
|-----------|------|
| N-01(配置引擎 C 堆必失败) | `../子系统设计/配置文件设计说明.md`(v1.13,§6.2/§8) |
| N-03a/N-03b/N-07/N-08(IWDG、`configASSERT`、两个钩子、4 个 Fault 现场) | `../子系统设计/看门狗设计说明.md`(§4.4、§10.1/§10.2)+ AGENTS.md「平台约束」 |
| N-10(UI accept 门控) | `../UI设计/RemoDispBus协议与接收设计说明.md` §3 + 看门狗设计说明 §11.4 |
| P1-1(ADC 数据路径) | `../子系统设计/ADC采样节拍设计说明.md` §10/§11 + `App/algo/README.md` |
| N-17(栈/堆水位观测) | `../内存管理/RAM使用分析与优化方案.md` + `../测试说明/实机测试实施指南.md` |
| N-18(本报告口径:无 CI) | `../测试说明/测试开发标准流程.md` §9 + `../环境与部署/CI配置指南.md` |
| P2-13 / P2-14 / P2-15 | AGENTS.md(`test-suite-switch`、`keil-build`、`stlink-flash`)+ `../测试说明/ST-LINK_CLI_复位使用说明.md` |
| D-2 / D-3 / D-4 / D-5 / D-6 / D-7 | `../文档规范.md` + `../代码规范/pre-commit使用说明.md`;断链由 `scripts/check_doc_links.py` 门禁接管(`--all` = 129 文档 / 0 失效) |
| U4(golden 回归) | `simulator/tests/` 四脚本 + `../UI设计/UI_自动化测试流程.md` |
| R-1 / R-7 | `../子系统设计/看门狗设计说明.md` + AGENTS.md 任务表(`scripts/gen_task_table.py` 生成区) |
| F-1 / F-2 / F-3(LVGL 死代码裁剪,2026-10-11) | `../内存管理/RAM使用分析与优化方案.md` §2.6.4(对象级前后对照 + 上游漏守卫的配套补丁);`../UI设计/UI_设计架构.md` §1.4 第 10 条(**两份 `lv_conf.h` 必须同一特性子集**这条新硬约束);`../UI设计/UI_自动化测试流程.md` 变更记录(池水位基线 5440/5520 → 5368/5448 及理由) |
+13 -3
View File
@@ -198,9 +198,19 @@ def check_file(path):
v.append((ln, 'S10 3.3.1', '函数名须全小写: %s' % name))
# S11 (2.3.5) 禁止相对/绝对路径 include(基于原始文本,字符串掩码会吃掉引号)
for m in re.finditer(r'^\s*#\s*include\s*[<"]([^">]+)[">]', text, re.M):
inc = m.group(1)
if '/' in inc or '\\' in inc or '..' in inc:
# 规则本意 = 禁"用路径找文件":`#include "../../x.h"`、`#include "App/foo/x.h"`、
# `#include <C:/x.h>`、`#include <..\..\x.h>`。
# ⚠ 2026-10-11 细化:原实现把**任何含 `/` 的 include** 都判违规,于是
# `<sys/stat.h>` 这类**尖括号系统头**也被误报(模拟器侧两处即中招)。
# 现按形态区分:尖括号 = 系统/依赖头,允许带子目录(由 -I 解析);
# 引号 = 项目头,必须裸文件名(带 `/` 或 `\` 即违规)。`..`/绝对路径两种形态都禁。
for m in re.finditer(r'^\s*#\s*include\s*([<"])([^">]+)[">]', text, re.M):
form, inc = m.group(1), m.group(2)
bad = ('..' in inc or '\\' in inc or inc.startswith('/')
or re.match(r'^[A-Za-z]:', inc) is not None)
if form == '"' and '/' in inc:
bad = True
if bad:
v.append((_line(text, m.start()), 'S11 2.3.5',
'禁止相对/绝对路径 include: %s' % inc))
+278
View File
@@ -0,0 +1,278 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""两份 lv_conf.h 特性子集一致性门禁(2026-10-11 加,随 F-1/F-2/F-3 落地)。
背景:`Lib/lv_conf.h`(MCU)与 `simulator/lv_conf.h`(PC)是**两份独立文件**,
此前长期漂移——模拟器早已 I1-only,MCU 端却让 11 个 `LV_DRAW_SW_SUPPORT_*`、
5 个未用控件类、`LV_USE_THEME_SIMPLE`、`LV_USE_FLEX/GRID` 全靠
`lv_conf_internal.h` 的默认值隐式生效,把用不到的代码整份链进固件
= 实测 **64,576 B 死代码**(旧镜像的 23.5%)。更糟的是 golden 回归跑的是
模拟器那份配置,**验证不到固件实际编译的特性子集**。
判定口径(关键):比对的是**生效值**,不是"文件里写了什么"——
文件里没定义的宏,取 `Lib/lvgl/src/lv_conf_internal.h` 的默认值。
所以"MCU 没写、模拟器写了 0"会被算成 MCU=1 → 直接判 FAIL,这正是原事故形态。
四类问题:
MISMATCH 生效值不同 → FAIL(特性子集不一致,golden 回归无效)
INVALID 写了 LVGL 不认的宏名 → FAIL(如 9.3 没有 LV_USE_THEME_BASIC,写了不生效)
DUPLICATE 同一文件里定义两次 → FAIL(后者生效,极易误读)
IMPLICIT 一边靠默认值生效 → 警告(不 FAIL;若默认值与另一边不同则已升级为 MISMATCH)
用法:
python scripts/check_lv_conf_sync.py # 检查(默认)
python scripts/check_lv_conf_sync.py --list # 打印全量对照表
python scripts/check_lv_conf_sync.py --mcu A --sim B --internal C
退出码:0 = 一致;1 = 有不一致/无效/重复;2 = 环境错误(文件缺失)
"""
import argparse
import io
import os
import re
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEF_MCU = os.path.join(REPO, "Lib", "lv_conf.h")
DEF_SIM = os.path.join(REPO, "simulator", "lv_conf.h")
DEF_INTERNAL = os.path.join(REPO, "Lib", "lvgl", "src", "lv_conf_internal.h")
DEF_LVGL_SRC = os.path.join(REPO, "Lib", "lvgl", "src")
RE_DEFINE = re.compile(r"^[ \t]*#[ \t]*define[ \t]+(LV_[A-Za-z0-9_]+)[ \t]*(.*)$")
RE_IFNDEF = re.compile(r"^[ \t]*#[ \t]*ifndef[ \t]+(LV_[A-Za-z0-9_]+)[ \t]*$")
RE_ANYIF = re.compile(r"^[ \t]*#[ \t]*if(def|ndef)?\b")
RE_ENDIF = re.compile(r"^[ \t]*#[ \t]*endif\b")
RE_NUM = re.compile(r"^(?:0[xX][0-9a-fA-F]+|[-+]?\d+)[uUlL]*$")
# 两份文件自己的 include 保护宏,不参与比对
IGNORE_EXACT = {"LV_CONF_H"}
# 只属于某一端的宏(前缀匹配):模拟器驱动 / 演示 / 头文件替换 / OS 抽象细节。
# 这些不参与"特性子集"一致性判定。
IGNORE_PREFIX = (
"LV_USE_SDL", "LV_SDL_", # SDL2 驱动(仅模拟器)
"LV_USE_LINUX_", "LV_USE_WINDOWS", "LV_USE_QNX", "LV_USE_UEFI",
"LV_USE_WAYLAND", "LV_USE_GLFW", "LV_USE_NUTTX", "LV_USE_OPENGLES",
"LV_USE_EVDEV", "LV_USE_LIBINPUT", "LV_USE_GENERIC_MIPI",
"LV_USE_FT81X", "LV_USE_LOVYAN_GFX",
"LV_USE_DEMO_", # 演示工程
"LV_STDINT_INCLUDE", "LV_STDDEF_INCLUDE", "LV_STDBOOL_INCLUDE",
"LV_INTTYPES_INCLUDE", "LV_LIMITS_INCLUDE", "LV_STDARG_INCLUDE",
"LV_OS_PRIORITY", "LV_OS_TASK_NAME", # LV_USE_OS=0 时惰性
"LV_FONT_CUSTOM_INCLUDE", "LV_FONT_USE_CUSTOM_INCLUDE",
)
# 允许两端不同(已核对理由):仍会打印,但不判 FAIL。
ALLOW_DIFF = {
"LV_USE_SNAPSHOT": "模拟器 --screenshot 需要 lv_snapshot;固件不用(省 Flash)",
"LV_OBJ_STYLE_CACHE": "模拟器开样式缓存便于调试;固件靠默认",
"LV_USE_OBJ_NAME": "模拟器调试辅助(对象命名);固件不需要",
"LV_USE_OBJ_ID": "模拟器调试辅助;固件不需要",
"LV_OBJ_ID_AUTO_ASSIGN": "随 LV_USE_OBJ_ID",
"LV_USE_OBJ_ID_BUILTIN": "随 LV_USE_OBJ_ID",
"LV_USE_ASSERT_NULL": "固件关断言省体积;模拟器保留有助定位",
"LV_USE_ASSERT_MALLOC": "同上",
"LV_USE_ASSERT_USER_CODE": "同上",
"LV_USE_ASSERT_OBJ": "同上",
"LV_USE_ASSERT_STYLE": "同上",
"LV_USE_ASSERT_LAYOUT": "同上",
}
def strip_comment(text):
"""去掉行内 /* */ 与 // 注释(不跨行)。"""
text = re.sub(r"/\*.*?\*/", "", text)
text = re.sub(r"//[^\"]*$", "", text)
return text.strip()
def read_lines(path):
"""按 UTF-8 读(MCU 那份带 BOM,用 utf-8-sig 兼容)。"""
with io.open(path, "r", encoding="utf-8-sig", errors="replace") as f:
return f.read().split("\n")
def parse_defines(path):
"""返回 (name -> 首个定义值, name -> 定义次数, 出现顺序)。"""
vals, counts, order = {}, {}, []
for line in read_lines(path):
m = RE_DEFINE.match(line)
if not m:
continue
name, raw = m.group(1), strip_comment(m.group(2))
counts[name] = counts.get(name, 0) + 1
if name not in vals:
vals[name] = raw
order.append(name)
return vals, counts, order
def parse_defaults(path):
"""从 lv_conf_internal.h 抽**兜底**默认值。
该文件每个选项的形状是:
#ifndef LV_X
#ifdef CONFIG_LV_X
#define LV_X CONFIG_LV_X <- ESP-IDF 兼容分支,不是默认值
#else
#define LV_X 0 <- 真正的兜底默认值
#endif
#endif
所以必须扫完整块、取**非 CONFIG_ 的字面量**(多个取最后一个)。
"""
lines = read_lines(path)
defaults = {}
i, n = 0, len(lines)
while i < n:
m = RE_IFNDEF.match(lines[i])
if not m:
i += 1
continue
name = m.group(1)
block, depth, j = [], 0, i + 1
while j < n:
line = lines[j]
if RE_ANYIF.match(line):
depth += 1
elif RE_ENDIF.match(line):
if depth == 0:
break
depth -= 1
block.append(line)
j += 1
cands = []
for line in block:
d = RE_DEFINE.match(line)
if d and d.group(1) == name:
cands.append(strip_comment(d.group(2)) or "1")
if cands:
literal = [c for c in cands if not c.startswith("CONFIG_")]
defaults[name] = literal[-1] if literal else cands[-1]
# 注意:不能跳到 j+1——lv_conf_internal.h 自身有 `#ifndef LV_CONF_INTERNAL_H`
# 大保护块,跳过去会把整份文件吞掉(实测 defaults 变空)。逐行前进,
# 让嵌套的 `#ifndef` 也能被扫到。
i += 1
return defaults
def build_symbols(src_dir):
"""扫 LVGL 头文件里的数值宏(LV_OS_NONE / LV_STDLIB_BUILTIN / LV_TXT_ENC_UTF8 等)。"""
symbols = {}
for root, _dirs, files in os.walk(src_dir):
for fn in files:
if not fn.endswith(".h"):
continue
try:
lines = read_lines(os.path.join(root, fn))
except OSError:
continue
for line in lines:
m = RE_DEFINE.match(line)
if not m:
continue
raw = strip_comment(m.group(2))
if RE_NUM.match(raw):
symbols.setdefault(m.group(1), raw)
return symbols
def normalize(value, symbols, depth=0):
"""归一化取值:去括号/整数后缀/空白,符号名解析成数值,便于跨文件比对。"""
v = (value or "").strip()
while v.startswith("(") and v.endswith(")"):
inner = v[1:-1].strip()
if not inner:
break
v = inner
v = re.sub(r"\s+", " ", v)
if RE_NUM.match(v):
v = re.sub(r"[uUlL]+$", "", v)
return str(int(v, 0))
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", v):
if v in symbols and depth < 4:
return normalize(symbols[v], symbols, depth + 1)
return v
return re.sub(r"(\d)[uUlL]+\b", r"\1", v)
def main(argv=None):
ap = argparse.ArgumentParser(description="两份 lv_conf.h 特性子集一致性门禁")
ap.add_argument("--mcu", default=DEF_MCU)
ap.add_argument("--sim", default=DEF_SIM)
ap.add_argument("--internal", default=DEF_INTERNAL)
ap.add_argument("--lvgl-src", default=DEF_LVGL_SRC)
ap.add_argument("--list", action="store_true", help="打印全量对照表")
a = ap.parse_args(argv)
for p in (a.mcu, a.sim, a.internal):
if not os.path.isfile(p):
print("[ERROR] 文件不存在: %s" % p)
return 2
mcu, mcu_cnt, _ = parse_defines(a.mcu)
sim, sim_cnt, _ = parse_defines(a.sim)
defaults = parse_defaults(a.internal)
symbols = build_symbols(a.lvgl_src)
names = sorted(set(mcu) | set(sim))
checked = [n for n in names if n not in IGNORE_EXACT and not n.startswith(IGNORE_PREFIX)]
bad = []
warn = []
rows = []
for n in checked:
in_mcu, in_sim = n in mcu, n in sim
vm = mcu.get(n, defaults.get(n))
vs = sim.get(n, defaults.get(n))
if vm is None or vs is None:
bad.append(("UNKNOWN", n, "取不到生效值(MCU=%s / SIM=%s)" % (vm, vs)))
continue
nm, ns = normalize(vm, symbols), normalize(vs, symbols)
rows.append((n, "显式" if in_mcu else "默认", nm, "显式" if in_sim else "默认", ns))
if nm != ns:
if n in ALLOW_DIFF:
print("[allow] %-40s MCU=%s SIM=%s (%s)" % (n, nm, ns, ALLOW_DIFF[n]))
else:
bad.append(("MISMATCH", n, "MCU=%s(%s) vs SIM=%s(%s)"
% (nm, "显式" if in_mcu else "默认", ns, "显式" if in_sim else "默认")))
if in_mcu != in_sim:
side = "MCU" if not in_mcu else "SIM"
warn.append(("IMPLICIT", n, "%s 端未显式定义,靠 lv_conf_internal 默认 = %s"
% (side, ns if not in_mcu else nm)))
known = set(defaults) | set(symbols)
for label, vals in (("MCU", mcu), ("SIM", sim)):
for n in sorted(vals):
if n in known or n in IGNORE_EXACT or n.startswith(IGNORE_PREFIX):
continue
bad.append(("INVALID", n, "%s 端定义了 LVGL 不认的宏(写了不生效)" % label))
for label, cnt in (("MCU", mcu_cnt), ("SIM", sim_cnt)):
for n in sorted(cnt):
if cnt[n] > 1:
bad.append(("DUPLICATE", n, "%s 端定义 %d 次(生效值 = 最后一次)" % (label, cnt[n])))
if a.list:
print("%-42s %-8s %-24s %-8s %s" % ("宏", "MCU 来源", "MCU 生效值", "SIM 来源", "SIM 生效值"))
for r in rows:
print("%-42s %-8s %-24s %-8s %s" % r)
print("对照 %d 项(忽略端专属宏 %d 项)" % (len(rows), len(names) - len(checked)))
for kind, n, why in warn:
print("[warn] %-40s %s" % (n, why))
if warn:
print("-> IMPLICIT 不判失败,但默认值随 LVGL 版本变;建议显式固化")
if not bad:
print("PASS: 两份 lv_conf.h 特性子集一致(生效值比对 %d 项,警告 %d 项)"
% (len(rows), len(warn)))
return 0
print("FAIL: %d 项不一致" % len(bad))
for kind, n, why in bad:
print(" [%s] %-38s %s" % (kind, n, why))
print("-> 两端必须跑同一套特性子集,否则模拟器 golden 回归验证不到固件实际编译的配置")
print(" 规则与理由见 docs/UI设计/UI_设计架构.md §1.4 第 10 条")
return 1
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -141,4 +141,4 @@ if (Test-Path $exe) {
}
Write-Host "[build_sim] 构建退出码 0 但未找到 $exe" -ForegroundColor Red
exit 1


+12 -3
View File
@@ -1,4 +1,13 @@
/*
* 模块名称:模拟器调试日志 stub(dbg_log.h)
* 模块功能:覆盖 MCU 工程的 Drivers/BSP/dbg_log.h,把 DBG_* 直接映射到宿主 printf
* (模拟器无 sys_clock/RTC,日志走 PC 控制台)
* 适用平台:Windows / Linux / macOS(PC 模拟器)
* 作者:王建锋
* 创建日期:2026-08-31
* 修改记录:
* v1.0 2026-08-31 王建锋 创建:模拟器侧 DBG_* stub
*
* dbg_log.h — 模拟器专用调试日志 stub(覆盖 MCU 工程的 Drivers/BSP/dbg_log.h)
*
* 模拟器不使用 sys_clock.h / RTC,因此把 DBG_* 宏直接映射到 printf,
@@ -6,8 +15,8 @@
* DBG_TAG 仍需各 .c 文件在 include 前定义,这里保留该约定。
*/
#ifndef __DBG_LOG_H
#define __DBG_LOG_H
#ifndef DBG_LOG_H
#define DBG_LOG_H
#ifdef __cplusplus
extern "C" {
@@ -35,4 +44,4 @@ extern "C" {
}
#endif
#endif /* __DBG_LOG_H */
#endif /* DBG_LOG_H */
+32 -23
View File
@@ -1,4 +1,16 @@
/**
* 模块名称:LVGL 配置 —— PC 模拟器侧(simulator/lv_conf.h)
* 模块功能:LVGL v9.3.0 编译期特性裁剪与 SDL2 适配(160x160 1bpp 单色)
* 适用平台:Windows(CMake + SDL2)
* 作者:王建锋
* 创建日期:2026-08-31
* 修改记录:
* v1.0 2026-08-31 王建锋 创建:基于 Lib/lv_conf.h 适配 PC 模拟器
*
* ⚠ 本文件与 `Lib/lv_conf.h`(MCU 端)必须保持**同一特性子集**,已由门禁校验:
* `python scripts/check_lv_conf_sync.py`(pre-commit 钩子 lv-conf-sync,
* 两端任一改动都会触发)。规则正文见 docs/UI设计/UI_设计架构.md §1.4 第 10 条。
*
* @file lv_conf.h
* @brief LVGL v9.3.0 配置 — Windows SDL2 模拟器(160x160 1bpp 单色)
* @note 基于项目 Lib/lv_conf.h 适配,用于 PC 模拟器 UI 开发
@@ -51,7 +63,8 @@
/** 复杂绘制:主界面顶部开口浅 V 装饰含斜线(lv_line 斜段依赖 LV_DRAW_SW_COMPLEX),须与 MCU 端一致保持开启,否则两腿不渲染 */
#define LV_DRAW_SW_COMPLEX 1
#define LV_DRAW_SW_COMPLEX_GRADIENTS 0
/* 注:9.3 没有 LV_DRAW_SW_COMPLEX_GRADIENTS / LV_DRAW_SW_YIELD / LV_DRAW_SW_ASYNC
* (9.4 才引入),原定义无效,已删 */
#define LV_DRAW_SW_SHADOW_CACHE_SIZE 0
#define LV_DRAW_SW_CIRCLE_CACHE_SIZE 4 /* LV_DRAW_SW_COMPLEX 依赖非零(零长数组 C2466);与 MCU 端默认一致 */
#define LV_DRAW_LAYER_SIMPLE_BUF_SIZE 0
@@ -72,16 +85,13 @@
#define LV_DRAW_SW_SUPPORT_I1 1
#define LV_DRAW_SW_I1_LUM_THRESHOLD 127
/** 关闭 GPU/矢量绘制 */
/** 关闭 GPU/矢量绘制(注:9.3 无 LV_USE_DRAW_NEMA_GFX / PPA / EVE / NANOVG,
* 属更新版本 lv_conf_template 残留,原定义无效,已删) */
#define LV_USE_DRAW_VG_LITE 0
#define LV_USE_DRAW_DAVE2D 0
#define LV_USE_DRAW_NEMA_GFX 0
#define LV_USE_DRAW_PXP 0
#define LV_USE_DRAW_PPA 0
#define LV_USE_DRAW_DMA2D 0
#define LV_USE_DRAW_EVE 0
#define LV_USE_DRAW_G2D 0
#define LV_USE_DRAW_NANOVG 0
#define LV_USE_DRAW_OPENGLES 0
#define LV_USE_DRAW_SDL 0
#define LV_USE_THORVG 0
@@ -105,11 +115,10 @@
#define LV_OBJ_ID_AUTO_ASSIGN 1
#define LV_USE_OBJ_ID_BUILTIN 1
#define LV_USE_OBJ_PROPERTY 0
#define LV_USE_EXT_DATA 0
#define LV_USE_FLOAT 0
#define LV_USE_OBSERVER 0
#define LV_USE_TRANSLATION 0
#define LV_USE_COLOR_FILTER 0
/* 注:9.3 无 LV_USE_EXT_DATA / LV_USE_TRANSLATION(9.4 才有),原两行无效,已删 */
/*============================================================================
* LOGGING
@@ -126,7 +135,7 @@
/*============================================================================
* LAYOUTS
*============================================================================*/
#define LV_USE_FLEX 1
#define LV_USE_FLEX 0 /* 2026-10-11 与 MCU 端一致:全仓无 lv_obj_set_flex_* 调用 */
#define LV_USE_GRID 0
/*============================================================================
@@ -142,7 +151,6 @@
#define LV_USE_BMP 0
#define LV_USE_TJPGD 0
#define LV_USE_LIBJPEG_TURBO 0
#define LV_USE_LIBWEBP 0
#define LV_USE_SVG 0
/*============================================================================
@@ -190,7 +198,7 @@
#define LV_FONT_CUSTOM_DECLARE LV_FONT_DECLARE(ui_font_cn_12)
#define LV_FONT_DEFAULT &ui_font_cn_12
#define LV_USE_CUSTOM_FONT_DEFAULT 0
/* 注:9.3 无 LV_USE_CUSTOM_FONT_DEFAULT,原行无效,已删 */
#define LV_FONT_FMT_TXT_LARGE 0
#define LV_USE_FONT_COMPRESSED 0
#define LV_USE_FONT_PLACEHOLDER 1
@@ -207,11 +215,10 @@
#define LV_USE_3DTEXTURE 0
#define LV_USE_ANIMIMG 0
#define LV_USE_ARC 0
#define LV_USE_ARCLABEL 0
#define LV_USE_BAR 0
#define LV_USE_BARCODE 0
#define LV_USE_BUTTON 1
#define LV_USE_BUTTONMATRIX 1
#define LV_USE_BUTTON 0
#define LV_USE_BUTTONMATRIX 0
#define LV_USE_CALENDAR 0
#define LV_USE_CANVAS 0
#define LV_USE_CHART 0
@@ -219,13 +226,16 @@
#define LV_USE_DROPDOWN 0
#define LV_USE_FFMPEG 0
#define LV_USE_GIF 0
#define LV_USE_GLTF 0
#define LV_USE_GSTREAMER 0
#define LV_USE_IMAGE 1
/* 注:9.3 无 LV_USE_GLTF / LV_USE_GSTREAMER / LV_USE_ARCLABEL(更新版本残留),已删 */
/* 2026-10-11(F-2/F-3 对齐 MCU 端 Lib/lv_conf.h):本项目 UI 全部自绘,
* 三处代码(Drivers/BSP/UI、App、simulator)零控件构造函数、零 flex/grid 调用。
* 关掉后模拟器与固件跑的是同一套特性子集,golden 回归才能真正验证 MCU 配置。
* 若将来引入控件型页面(U2 原型库),两端同步逐项打开并复跑 ui-verify。 */
#define LV_USE_IMAGE 0
#define LV_USE_IMAGEBUTTON 0
#define LV_USE_IME_PINYIN 0
#define LV_USE_KEYBOARD 1
#define LV_USE_LABEL 1
#define LV_USE_KEYBOARD 0
#define LV_USE_LABEL 0
#define LV_LABEL_TEXT_SELECTION 0
#define LV_LABEL_LONG_TXT_HINT 0
#define LV_LABEL_WAIT_CHAR_COUNT 0
@@ -244,7 +254,7 @@
#define LV_USE_SPINBOX 0
#define LV_USE_SPINNER 0
#define LV_USE_SWITCH 0
#define LV_USE_TEXTAREA 1
#define LV_USE_TEXTAREA 0
#define LV_USE_TABLE 0
#define LV_USE_TABVIEW 0
#define LV_USE_TILEVIEW 0
@@ -288,13 +298,12 @@
#define LV_USE_WINDOWS 0
/*============================================================================
* THEMES (补充)
* THEMES 子选项(LV_USE_THEME_DEFAULT / SIMPLE 本体已在上面 THEMES 块定义,
* 此处勿重复定义——同文件重复 #define 后者生效,极易误读)
*============================================================================*/
#define LV_USE_THEME_DEFAULT 0
#define LV_THEME_DEFAULT_DARK 0
#define LV_THEME_DEFAULT_GROW 0
#define LV_THEME_DEFAULT_TRANSITION_TIME 0
#define LV_USE_THEME_SIMPLE 0
/*============================================================================
* EXPORTS
+55 -38
View File
@@ -1,4 +1,12 @@
/**
* 模块名称:PC 模拟器入口(simulator/main.c)
* 模块功能:在 PC 上以 SDL2 跑**与固件同一套** UI 源码,提供窗口调试与多种无头自检模式
* 适用平台:Windows(CMake + SDL2)
* 作者:王建锋
* 创建日期:2026-08-31
* 修改记录:
* v1.0 2026-08-31 王建锋 创建:基于 lv_port_pc_vscode 适配
*
* @file main.c
* @brief LVGL 模拟器入口 — 160x160 1bpp 单色菜单 UI
* @note 基于 lv_port_pc_vscode 适配,用于 PC 上 UI 开发与调试
@@ -69,30 +77,35 @@
static uint8_t g_shot_buf[SIM_SCREEN_W * SIM_SCREEN_H / 8];
/*
* @brief 离屏截图用空 delay 回调
* @note 默认 lv_delay_ms 会忙等 tick 前进,但离屏渲染时 tick 不会在等待期间推进,
* 导致死循环挂死。window 模式经 SDL 设了真实 delay,离屏这里用空实现代替。
* 函数功能:离屏截图用的空 delay 回调
* 入口参数:ms - 请求延时的毫秒数(未使用) uint32_t
* 返回值:无
* 说明:默认 lv_delay_ms 会忙等 tick 前进,但离屏渲染时 tick 不会在等待期间推进,
* 导致死循环挂死。window 模式经 SDL 设了真实 delay,离屏这里用空实现代替。
*/
static void shot_delay_cb(uint32_t ms)
{
static void shot_delay_cb(uint32_t ms) {
(void)ms;
}
/*
* @brief 离屏截图 flush 回调(不写真实设备,直接标记就绪)
* 函数功能:离屏截图 flush 回调(不写真实设备,直接标记就绪)
* 入口参数:disp - LVGL 显示对象 lv_display_t *
* area - 刷新区域(未使用) const lv_area_t *
* color_p - 像素缓冲(未使用) uint8_t *
* 返回值:无
*/
static void shot_flush_cb(lv_display_t * disp, const lv_area_t * area, uint8_t * color_p)
{
static void shot_flush_cb(lv_display_t * disp, const lv_area_t * area, uint8_t * color_p) {
(void)area;
(void)color_p;
lv_display_flush_ready(disp);
}
/*
* 键序列字符 → LVGL 键值(非键字符返回 0 = 忽略)
* 函数功能:键序列字符 → LVGL 键值映射(--keys 无头回归用)
* 入口参数:c - 键序列字符(u=↑ d=↓ l=← r=→ e=确认 s=返回) char
* 返回值:LVGL 键值;非键字符返回 0 = 忽略 uint32_t
*/
static uint32_t sim_map_key_char(char c)
{
static uint32_t sim_map_key_char(char c) {
switch (c) {
case 'u': return LV_KEY_UP;
case 'd': return LV_KEY_DOWN;
@@ -105,12 +118,14 @@ static uint32_t sim_map_key_char(char c)
}
/*
* @brief 待机屏底部时钟提供回调(平台差异:模拟器 = 宿主 time())
* 函数功能:待机屏底部时钟提供回调(平台差异:模拟器 = 宿主 time())
* 入口参数:buf - 输出缓冲(格式化时间串) char *
* len - 缓冲长度 size_t
* 返回值:无
*/
static void sim_time_cb(char *buf, size_t len)
{
static void sim_time_cb(char *buf, size_t len) {
time_t t = time(NULL);
struct tm tmv;
struct tm tmv = {0}; /* 显式清零:localtime_s/r 是"输出型"参数,静态分析按未初始化报 */
#if defined(_MSC_VER)
localtime_s(&tmv, &t);
@@ -123,14 +138,13 @@ static void sim_time_cb(char *buf, size_t len)
}
/*
* @brief 无窗口离屏渲染(可注入键序列)并导出末帧像素
* @param keys 键序列(u=上 d=下 l=左 r=右 e=确认 s=返回;空 = 不注键)
* @param path 输出文件(写入 160x160 I1 原始像素)
* @param standby 非 0 = 注键后强制进入待机屏再截图(--standby 模式)
* @return 0 成功,非 0 失败
* 函数功能:无窗口离屏渲染(可注入键序列)并导出末帧像素
* 入口参数:keys - 键序列(u=上 d=下 l=左 r=右 e=确认 s=返回;空 = 不注键) const char *
* path - 输出文件(写入 160x160 I1 原始像素) const char *
* standby - 非 0 = 注键后强制进入待机屏再截图(--standby 模式) int
* 返回值:0 - 成功;非 0 - 失败 int
*/
static int run_headless(const char * keys, const char * path, int standby)
{
static int run_headless(const char * keys, const char * path, int standby) {
setvbuf(stdout, NULL, _IONBF, 0);
lv_init();
lv_delay_set_cb(shot_delay_cb);
@@ -209,7 +223,7 @@ static int run_headless(const char * keys, const char * path, int standby)
uint32_t snap_h = snap->header.h;
/* I1 缓冲区布局:[palette 8B][pixel data],跳过 palette 只写像素 */
uint32_t pal_bytes = LV_COLOR_INDEXED_PALETTE_SIZE(LV_COLOR_FORMAT_I1) * (uint32_t)sizeof(lv_color32_t);
uint8_t * px = snap->data + pal_bytes;
const uint8_t * px = snap->data + pal_bytes; /* 只读:写文件时不再改动快照 */
uint32_t px_bytes = (uint32_t)snap->header.stride * (uint32_t)snap_h;
FILE *fp = fopen(path, "wb");
if (fp == NULL) {
@@ -239,13 +253,13 @@ static const char s_cfgtest_doc[] =
static const uint8_t s_cfgtest_ip[4] = { 10u, 0u, 0u, 1u };
/*
* @brief 配置轮询自检(无窗口、无需 LVGL/SDL,进程内闭环)
* @note 验证链:net_cfg_load(只读 INI)→ poll 建基线 → 伪造外部编辑
* → poll 两次(沉降去抖)→ 重载并更新 RAM 副本
* @return 0 PASS,非 0 FAIL
* 函数功能:配置轮询自检(无窗口、无需 LVGL/SDL,进程内闭环)
* 入口参数:无
* 返回值:0 - PASS;非 0 - FAIL int
* 说明:验证链 net_cfg_load(只读 INI)→ poll 建基线 → 伪造外部编辑
* → poll 两次(沉降去抖)→ 重载并更新 RAM 副本
*/
static int run_cfgtest(void)
{
static int run_cfgtest(void) {
int i;
const net_cfg_t *p_cfg;
@@ -367,13 +381,13 @@ static int run_cfgtest(void)
}
/*
* @brief 日志引擎自检(无窗口、进程内闭环)
* @note 验证链:清理旧档 → post N 条 → count/get 回读断言 + 宿主文件字节级校验
* (64B 定长、ISO 时间戳格式、epoch 字段 ≈ now、NUL 填充)
* @return 0 PASS,非 0 FAIL
* 函数功能:日志引擎自检(无窗口、进程内闭环)
* 入口参数:无
* 返回值:0 - PASS;非 0 - FAIL int
* 说明:验证链 清理旧档 → post N 条 → count/get 回读断言 + 宿主文件字节级校验
* (64B 定长、ISO 时间戳格式、epoch 字段 ≈ now、NUL 填充)
*/
static int run_logtest(void)
{
static int run_logtest(void) {
uint32_t ep;
char ts[SYSLOG_TS_LEN + 1u];
char desc[SYSLOG_DESC_MAX + 1u];
@@ -464,10 +478,13 @@ static int run_logtest(void)
}
/**
* @brief 程序入口
* 函数功能:模拟器入口(解析模式开关 → 窗口模式或无头自检)
* 入口参数:argc - 参数个数 int
* argv - 参数数组(--screenshot / --keys / --standby / --cfgtest /
* --logtest / --idle) char **
* 返回值:0 - 正常退出或自检 PASS;非 0 - 失败 int
*/
int main(int argc, char **argv)
{
int main(int argc, char **argv) {
/* 截图/自检模式:--screenshot / --keys / --standby / --cfgtest / --logtest */
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--screenshot") == 0 && (i + 1) < argc) {
+30 -60
View File
@@ -53,8 +53,7 @@ static int s_generation = 0; /* 网络代次(模拟器恒为 0
* 限定条件:无
* 函数说明:任何 Socket 操作前先调用;已初始化则直接返回
*/
static int ensure_wsa(void)
{
static int ensure_wsa(void) {
static int s_done = 0;
if (s_done == 0) {
@@ -75,8 +74,7 @@ static int ensure_wsa(void)
* 限定条件:s_socks 已定义
* 函数说明:线性扫描,返回首个 in_use == 0 的控制块索引
*/
static int alloc_fd(void)
{
static int alloc_fd(void) {
int i;
for (i = 0; i < NET_MAX_SOCKETS; i++) {
@@ -96,8 +94,7 @@ static int alloc_fd(void)
* 限定条件:两结构体布局一致(均 16 字节,字段偏移相同)
* 函数说明:字段偏移一致,按 sizeof 直接拷贝
*/
static void to_ws_addr(const struct net_sockaddr *addr, struct sockaddr_in *out)
{
static void to_ws_addr(const struct net_sockaddr *addr, struct sockaddr_in *out) {
memcpy(out, addr, sizeof(struct sockaddr_in));
}
@@ -110,8 +107,7 @@ static void to_ws_addr(const struct net_sockaddr *addr, struct sockaddr_in *out)
* 限定条件:两结构体布局一致(均 16 字节,字段偏移相同)
* 函数说明:字段偏移一致,按 sizeof 直接拷贝
*/
static void from_ws_addr(const struct sockaddr_in *in, struct net_sockaddr *out)
{
static void from_ws_addr(const struct sockaddr_in *in, struct net_sockaddr *out) {
memcpy(out, in, sizeof(struct sockaddr_in));
}
@@ -123,8 +119,7 @@ static void from_ws_addr(const struct sockaddr_in *in, struct net_sockaddr *out)
* 限定条件:无
* 函数说明:与 MCU 端 net_init 语义对齐,供上层统一调用
*/
int net_init(const char *ip, const char *mask, const char *gateway, const uint8_t *mac)
{
int net_init(const char *ip, const char *mask, const char *gateway, const uint8_t *mac) {
(void)ip;
(void)mask;
(void)gateway;
@@ -145,8 +140,7 @@ int net_init(const char *ip, const char *mask, const char *gateway, const uint8_
* 限定条件:无
* 函数说明:模拟器无网络重建,代次恒 0,使上层代次自愈逻辑空转
*/
uint32_t net_get_generation(void)
{
uint32_t net_get_generation(void) {
return (uint32_t)s_generation;
}
@@ -157,8 +151,7 @@ uint32_t net_get_generation(void)
* 限定条件:无
* 函数说明:模拟器不会触发,保留接口以对齐 API
*/
void net_generation_inc(void)
{
void net_generation_inc(void) {
s_generation++;
}
@@ -169,8 +162,7 @@ void net_generation_inc(void)
* 限定条件:无
* 函数说明:模拟器无 CH395F 中断,空操作
*/
int net_poll(void)
{
int net_poll(void) {
return 0;
}
@@ -183,8 +175,7 @@ int net_poll(void)
* 限定条件:Winsock 已就绪
* 函数说明:分配空闲控制块,创建 Winsock SOCKET 并置非阻塞
*/
int net_socket(int domain, int type)
{
int net_socket(int domain, int type) {
int idx;
int is_udp;
SOCKET ws;
@@ -238,8 +229,7 @@ int net_socket(int domain, int type)
* 限定条件:socket 已创建
* 函数说明:设置 SO_REUSEADDR 后 bind,便于断线后重监听复用端口
*/
int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen)
{
int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) {
struct sockaddr_in wsaddr;
int yes = 1;
@@ -273,8 +263,7 @@ int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen)
* 限定条件:socket 已绑定
* 函数说明:backlog 为 1(单客户端),监听 Socket 保持非阻塞
*/
int net_listen(int sockfd)
{
int net_listen(int sockfd) {
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) {
s_net_errno = NET_ERR_BADF;
return -1;
@@ -301,8 +290,7 @@ int net_listen(int sockfd)
* 函数说明:非阻塞监听下无新连接时返回 -1 并置 NET_ERR_WOULDBLOCK;
* 与 MCU 端不同,Winsock 的 accept 返回独立于监听 Socket 的新 fd
*/
int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen)
{
int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) {
struct sockaddr_in wscli;
int wslen;
SOCKET cws;
@@ -356,8 +344,7 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen)
* 而 accept 本就是非阻塞的、**没有 MCU 端的 RPC 成本** → 恒返回 1,
* 使上层门控在模拟器上等价于"改前行为"(逐位一致,不引入新语义)
*/
int net_can_accept(int sockfd)
{
int net_can_accept(int sockfd) {
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) {
return 0;
}
@@ -372,8 +359,7 @@ int net_can_accept(int sockfd)
* 函数说明:同 net_can_accept——Winsock recv 非阻塞、无 RPC 成本 → 恒返回 1,
* 上层门控在模拟器上不改变既有行为(含"每轮都 recv"的自愈特性)
*/
int net_can_read(int sockfd)
{
int net_can_read(int sockfd) {
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) {
return 0;
}
@@ -390,8 +376,7 @@ int net_can_read(int sockfd)
* 限定条件:socket 已创建
* 函数说明:非阻塞 Socket 上连接未完成时返回 -1 + NET_ERR_WOULDBLOCK
*/
int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen)
{
int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen) {
struct sockaddr_in wsaddr;
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) {
@@ -430,8 +415,7 @@ int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen)
* 函数说明:非阻塞单次发送(保持 UI 主循环不阻塞);协议帧小且对端主动
* 读取,实践中一次即发完
*/
int net_send(int sockfd, const void *buf, int len, int flags)
{
int net_send(int sockfd, const void *buf, int len, int flags) {
int r;
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) {
@@ -473,8 +457,7 @@ int net_send(int sockfd, const void *buf, int len, int flags)
* 函数说明:与 MCU 端语义一致——>0 有数据,0 对端关闭,<0 且 errno 区分
* "暂无数据"与"真错误"
*/
int net_recv(int sockfd, void *buf, int len, int flags)
{
int net_recv(int sockfd, void *buf, int len, int flags) {
int r;
(void)flags;
@@ -519,8 +502,7 @@ int net_recv(int sockfd, void *buf, int len, int flags)
* 限定条件:socket 已创建,buf/dest_addr 非空
*/
int net_sendto(int sockfd, const void *buf, int len, int flags,
const struct net_sockaddr *dest_addr, int addrlen)
{
const struct net_sockaddr *dest_addr, int addrlen) {
struct sockaddr_in wsaddr;
int r;
@@ -563,8 +545,7 @@ int net_sendto(int sockfd, const void *buf, int len, int flags,
* 限定条件:socket 已创建,buf 非空
*/
int net_recvfrom(int sockfd, void *buf, int len, int flags,
struct net_sockaddr *src_addr, int *addrlen)
{
struct net_sockaddr *src_addr, int *addrlen) {
struct sockaddr_in wsaddr;
int wslen;
int r;
@@ -606,8 +587,7 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags,
* 限定条件:socket 已打开(in_use == 1)
* 函数说明:关闭 Winsock 句柄并释放控制块(in_use 清零,net_get_sock 转返回 NULL)
*/
int net_close(int sockfd)
{
int net_close(int sockfd) {
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
s_net_errno = NET_ERR_BADF;
return -1;
@@ -636,8 +616,7 @@ int net_close(int sockfd)
* 限定条件:socket 已创建
* 函数说明:模拟器以非阻塞 I/O 直接感知事件,回调仅存储不主动触发
*/
int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg)
{
int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg) {
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) {
s_net_errno = NET_ERR_BADF;
return -1;
@@ -656,8 +635,7 @@ int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg)
* 返回值:最近一次 net_* 操作设置的错误码(NET_OK / NET_ERR_xxx)
* 限定条件:无
*/
int net_get_errno(void)
{
int net_get_errno(void) {
return s_net_errno;
}
@@ -669,8 +647,7 @@ int net_get_errno(void)
* 限定条件:无
* 函数说明:逐段解析后按 NET_HTONL 转网络字节序(与 MCU 端实现一致)
*/
uint32_t net_inet_addr(const char *cp)
{
uint32_t net_inet_addr(const char *cp) {
uint32_t addr = 0;
int i;
@@ -701,8 +678,7 @@ uint32_t net_inet_addr(const char *cp)
* 返回值:buf 指针(buf 为 NULL 时返回 NULL)
* 限定条件:buf 非空且长度足够
*/
char *net_inet_ntoa(uint32_t addr, char *buf)
{
char *net_inet_ntoa(uint32_t addr, char *buf) {
uint32_t naddr = NET_NTOHL(addr);
if (buf == NULL) {
@@ -724,8 +700,7 @@ char *net_inet_ntoa(uint32_t addr, char *buf)
* 返回值:网络序端口
* 限定条件:无
*/
uint16_t net_htons(uint16_t hostshort)
{
uint16_t net_htons(uint16_t hostshort) {
return NET_HTONS(hostshort);
}
@@ -735,8 +710,7 @@ uint16_t net_htons(uint16_t hostshort)
* 返回值:主机序端口
* 限定条件:无
*/
uint16_t net_ntohs(uint16_t netshort)
{
uint16_t net_ntohs(uint16_t netshort) {
return NET_NTOHS(netshort);
}
@@ -746,8 +720,7 @@ uint16_t net_ntohs(uint16_t netshort)
* 返回值:网络序 IP
* 限定条件:无
*/
uint32_t net_htonl(uint32_t hostlong)
{
uint32_t net_htonl(uint32_t hostlong) {
return NET_HTONL(hostlong);
}
@@ -757,8 +730,7 @@ uint32_t net_htonl(uint32_t hostlong)
* 返回值:主机序 IP
* 限定条件:无
*/
uint32_t net_ntohl(uint32_t netlong)
{
uint32_t net_ntohl(uint32_t netlong) {
return NET_NTOHL(netlong);
}
@@ -769,8 +741,7 @@ uint32_t net_ntohl(uint32_t netlong)
* 限定条件:无
* 函数说明:上层据此判断"监听 Socket 是否随客户端一同失效"(单连接模型)
*/
net_sock_t *net_get_sock(int sockfd)
{
net_sock_t *net_get_sock(int sockfd) {
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) {
return NULL;
}
@@ -790,8 +761,7 @@ net_sock_t *net_get_sock(int sockfd)
* 限定条件:socket 已创建
* 函数说明:经 Winsock getsockname 读取实际绑定地址
*/
int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen)
{
int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen) {
struct sockaddr_in wsaddr;
int wslen;
+3 -6
View File
@@ -25,8 +25,7 @@ static lv_indev_t *g_kbd_indev = NULL;
* 2. 创建键盘输入设备及默认输入组
* 3. 不创建鼠标/触摸设备(模拟器仅用键盘导航)
*/
int sim_port_init(int32_t hor_res, int32_t ver_res)
{
int sim_port_init(int32_t hor_res, int32_t ver_res) {
/* 创建默认输入组 */
lv_group_t *g = lv_group_create();
if (g == NULL) {
@@ -65,8 +64,7 @@ int sim_port_init(int32_t hor_res, int32_t ver_res)
* 返回值:显示设备指针 lv_display_t * 可能为 NULL
* 限定条件:sim_port_init() 已成功调用
*/
void *sim_port_get_display(void)
{
void *sim_port_get_display(void) {
return (void *)lv_display_get_default();
}
@@ -83,8 +81,7 @@ void *sim_port_get_display(void)
* 转换,直接读 get_buf_active 不可靠;读回屏幕呈现像素最稳,
* 且完全避开快照离屏重渲染(SDL 驱动下会卡派发循环)
*/
int sim_port_get_screen_mono(uint8_t *buf, uint32_t buf_size)
{
int sim_port_get_screen_mono(uint8_t *buf, uint32_t buf_size) {
if (buf == NULL) {
return -1;
}
+3 -3
View File
@@ -4,8 +4,8 @@
* 适用平台:Windows / Linux / macOS (SDL2)
* 说明:替代 MCU 端的 ui_port.c,用于 PC 上 UI 开发与调试
*/
#ifndef __SIM_PORT_H
#define __SIM_PORT_H
#ifndef SIM_PORT_H
#define SIM_PORT_H
#include <stdint.h>
@@ -47,4 +47,4 @@ int sim_port_get_screen_mono(uint8_t *buf, uint32_t buf_size);
}
#endif
#endif /* __SIM_PORT_H */
#endif /* SIM_PORT_H */
+13 -6
View File
@@ -32,16 +32,16 @@ static const uint32_t s_caps[SYSLOG_COUNT] = { SYSLOG_RUN_MAX, SYSLOG_SELFTEST_M
*/
static void s_now(char *p_ts, uint32_t *p_epoch) {
time_t t = time(NULL);
struct tm tmv;
struct tm tmv = {0}; /* 显式清零:localtime_s/r 是"输出型"参数,静态分析按未初始化报 */
#if defined(_WIN32)
localtime_s(&tmv, &t);
#else
localtime_r(&t, &tmv);
#endif
(void)sprintf(p_ts, "%04d-%02d-%02d %02d:%02d:%02d",
tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday,
tmv.tm_hour, tmv.tm_min, tmv.tm_sec);
(void)snprintf(p_ts, SYSLOG_TS_LEN + 1u, "%04d-%02d-%02d %02d:%02d:%02d",
tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday,
tmv.tm_hour, tmv.tm_min, tmv.tm_sec);
*p_epoch = (uint32_t)t;
}
@@ -68,7 +68,9 @@ static int s_append(syslog_id_t id, const char *desc) {
s_now(ts, &epoch);
memcpy(rec, ts, SYSLOG_TS_LEN);
rec[SYSLOG_TS_LEN] = ' ';
(void)sprintf((char *)(rec + SYSLOG_EPOCH_OFFS), "%010u", (unsigned int)epoch);
/* +1 = 容纳 NUL;字段本身固定 10 位数字,与旧 sprintf 写入字节数完全一致 */
(void)snprintf((char *)(rec + SYSLOG_EPOCH_OFFS), SYSLOG_EPOCH_LEN + 1u,
"%010u", (unsigned int)epoch);
rec[SYSLOG_DESC_OFFS - 1u] = ' ';
dl = strlen(desc);
if (dl > SYSLOG_DESC_MAX) {
@@ -185,7 +187,12 @@ int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch) {
uint32_t v = 0u;
uint32_t i;
if (p_epoch == NULL || syslog_count(id) == 0u) {
/* 先判空再写:旧写法 `if (p_epoch == NULL || ...)` 之后立刻 *p_epoch = 0,
* 一旦调用方真传 NULL 就是空指针解引用(cppcheck nullPointerRedundantCheck 实报) */
if (p_epoch == NULL) {
return -1;
}
if (syslog_count(id) == 0u) {
*p_epoch = 0u;
return -1;
}
+3 -6
View File
@@ -21,8 +21,7 @@
* 限定条件:无
* 函数说明:远端按键码与 LVGL 按键码一一对应
*/
static uint32_t sim_map_key(uint8_t remo_key)
{
static uint32_t sim_map_key(uint8_t remo_key) {
switch (remo_key) {
case UI_KEY_UP: return LV_KEY_UP;
case UI_KEY_DOWN: return LV_KEY_DOWN;
@@ -43,8 +42,7 @@ static uint32_t sim_map_key(uint8_t remo_key)
* 函数说明:窗口模式显示缓冲经 I1→ARGB8888 转换,不能直读,
* 故读回屏幕像素再按红通道阈值打包 1bpp(布局与 MCU 端一致)
*/
int ui_net_sink_get_lcdmem(uint8_t *out, uint32_t out_size)
{
int ui_net_sink_get_lcdmem(uint8_t *out, uint32_t out_size) {
if (out == NULL || out_size < UI_NET_SINK_MONO_SIZE) {
return -1;
}
@@ -58,8 +56,7 @@ int ui_net_sink_get_lcdmem(uint8_t *out, uint32_t out_size)
* 限定条件:默认输入组已建立
* 函数说明:映射为 LV_KEY_* 后经 lv_group_send_data 发向焦点对象
*/
void ui_net_sink_inject_key(uint8_t remo_key)
{
void ui_net_sink_inject_key(uint8_t remo_key) {
lv_group_t *g;
uint32_t key;
+3 -1
View File
@@ -106,7 +106,9 @@ STRIDE = W // 8 # I1:20 字节/行
# 页面结构变更导致漂移时,先确认新值合理再更新此常量(内存回归门禁)。
# 2026-09-14: 4864 → 5520(+656B)= run_headless 新增 ui_standby_init():待机模块
# 常驻屏 s_scr + 容器 s_cont 两个 lv_obj,随菜单基线同移(见 check_menu_rows.py)。
POOL_AFTER_ROUNDTRIP = 5520
# 2026-10-11: 5520 → 5448(−72B)= 裁剪 LVGL 控件类(F-2),随菜单基线 5440→5368 同移;
# **一次性残留不变,仍是 80 B**(5448 − 5368),说明泄漏特征未变,只是每对象样式表更小。
POOL_AFTER_ROUNDTRIP = 5448
def run_sim(seq, out):
+5 -1
View File
@@ -57,7 +57,11 @@ ROOT_X0, ROOT_Y0, ROOT_W, ROOT_H = 0, 44, 66, 107
# 先确认新值合理再更新此常量(内存回归门禁,见 docs/UI设计/UI_自动化测试流程.md)
# 2026-09-14: 4784 → 5440(+656B)= run_headless 新增 ui_standby_init():待机模块
# 常驻屏 s_scr + 容器 s_cont 两个 lv_obj(先于池测量创建),属有意结构变更。
MENU_POOL_BASELINE = 5440
# 2026-10-11: 5440 → 5368(−72B)= 裁剪 LVGL 控件类(F-2:label/button/buttonmatrix/
# bar/textarea 全关,simulator/lv_conf.h 与 Lib/lv_conf.h 对齐为同一特性子集)。
# 像素逐字节未变(11 页黄金参考在**裁剪前**生成、裁剪后全部一致);差值来自
# lv_theme_apply 不再为已不存在的控件类登记样式条目 → 每对象样式表更小。更优。
MENU_POOL_BASELINE = 5368
def load(path):
+275
View File
@@ -0,0 +1,275 @@
/*
* 模块名称:保护元件框架(L3,P1 子集)主机侧单元测试
* 模块功能:在 PC 上对 App/algo/prot_elm.c 的判据核做离线验证。
*
* 被测对象:真实源文件(#include 原样纳入):
* prot_elm.c + prot_set.c(判据吃定值 RAM)
* phasor.c + sample_ring.c(prot_elm.c 链接依赖;本测试只走
* prot_elm_eval 纯判据路径,不依赖 run() 适配层)
*
* 覆盖点(docs/算法/继电保护功能复现规划.md §3.3 L3-1/3/4/6、§8 验收):
* TC-PE-001 init:全状态清零、动作位图 0
* TC-PE-002 过量滞回 + 定时限:1.01× 定值启动、延时到点动作、
* 返回系数内保持、跌破返回门槛释放(1.2×/0.9× 验收口径的核)
* TC-PE-003 延时 0:启动同拍即动作
* TC-PE-004 欠量方向(UV):低于定值启动、高于返回门槛释放
* TC-PE-005 投退=0:不判据、状态复位
* TC-PE-006 定值闭锁(L5-1 联动):locked → 全元件复位;解锁后恢复
* TC-PE-007 时基回绕:跨 0xFFFFFFFF 的延时判定仍正确
* TC-PE-008 动作位图聚合
* TC-PE-009 守卫:state(NULL)/越界编号、name 越界
*
* 通过:全部 TC PASS(main 返回 0)。
* 创建日期:2026-10-11
*/
#include <stdint.h>
#include <stdio.h>
static int s_total = 0, s_passed = 0, s_failed = 0;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
} while (0)
#include "prot_set.c"
#include "sample_ring.c"
#include "phasor.c"
#include "prot_elm.c"
/* 中性测量基线:电流 1.0 A、电压 100 V、3U0 0 V(全部不越限) */
static void mk_base(float m[PE_COUNT]) {
uint32_t i;
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
m[i] = 1.0f;
}
m[PE_OV] = 100.0f;
m[PE_UV] = 100.0f;
m[PE_3U0] = 0.0f;
}
static void tc_init_clear(void) {
prot_set_init();
prot_elm_init();
CHECK(prot_elm_active_mask() == 0u, "TC-PE-001 init: active mask 0");
{
prot_elm_state_t st;
uint32_t i;
int clean = 1;
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
prot_elm_state((prot_elm_id_t)i, &st);
if ((st.picked != 0u) || (st.asserted != 0u) || (st.enabled != 0u)) {
clean = 0;
}
}
CHECK(clean, "TC-PE-001 init: all element states clear");
}
}
static void tc_over_hysteresis(void) {
float m[PE_COUNT];
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_OC1_DLY, PROT_SET_Q10(100.0)); /* 100 ms */
/* 默认 OC1_CUR=10.0 A,RET_OVER=0.95 → 返回门槛 ≈9.492 */
mk_base(m);
m[PE_OC1] = 9.9f;
prot_elm_eval(1000u, m);
prot_elm_state(PE_OC1, &st);
CHECK((st.picked == 0u) && (st.enabled != 0u),
"TC-PE-002 0.99x set: no pickup");
m[PE_OC1] = 10.1f;
prot_elm_eval(1001u, m);
prot_elm_state(PE_OC1, &st);
CHECK((st.picked != 0u) && (st.asserted == 0u),
"TC-PE-002 1.01x set: pickup before delay");
prot_elm_eval(1100u, m); /* 1100-1001 = 99 ms */
prot_elm_state(PE_OC1, &st);
CHECK(st.asserted == 0u, "TC-PE-002 not asserted at 99ms");
prot_elm_eval(1101u, m); /* 100 ms 整 */
prot_elm_state(PE_OC1, &st);
CHECK(st.asserted != 0u, "TC-PE-002 asserted at 100ms");
m[PE_OC1] = 9.7f; /* 返回门槛(9.492)与动作门槛(10)之间 */
prot_elm_eval(1200u, m);
prot_elm_state(PE_OC1, &st);
CHECK((st.picked != 0u) && (st.asserted != 0u),
"TC-PE-002 hysteresis: holds between return and pickup threshold");
m[PE_OC1] = 9.4f; /* 跌破返回门槛 */
prot_elm_eval(1300u, m);
prot_elm_state(PE_OC1, &st);
CHECK((st.picked == 0u) && (st.asserted == 0u),
"TC-PE-002 below return threshold: drops out");
}
static void tc_zero_delay(void) {
float m[PE_COUNT];
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_OC3_DLY, PROT_SET_Q10(0.0));
mk_base(m);
m[PE_OC3] = 2.1f; /* OC3 定值 2.0 */
prot_elm_eval(5000u, m);
prot_elm_state(PE_OC3, &st);
CHECK((st.picked != 0u) && (st.asserted != 0u),
"TC-PE-003 dly=0: asserted on the same beat as pickup");
}
static void tc_under_direction(void) {
float m[PE_COUNT];
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_UV_DLY, PROT_SET_Q10(50.0));
/* 默认 UV_VOL=20.0,RET_UNDER=1.05 → 返回门槛 ≈20.996 */
mk_base(m);
m[PE_UV] = 25.0f;
prot_elm_eval(2000u, m);
prot_elm_state(PE_UV, &st);
CHECK(st.picked == 0u, "TC-PE-004 UV above set: no pickup");
m[PE_UV] = 19.0f;
prot_elm_eval(2001u, m);
prot_elm_state(PE_UV, &st);
CHECK(st.picked != 0u, "TC-PE-004 UV below set: pickup");
prot_elm_eval(2051u, m); /* 50 ms */
prot_elm_state(PE_UV, &st);
CHECK(st.asserted != 0u, "TC-PE-004 UV asserted after delay");
m[PE_UV] = 20.5f; /* 20 与 20.996 之间:保持 */
prot_elm_eval(2100u, m);
prot_elm_state(PE_UV, &st);
CHECK(st.asserted != 0u, "TC-PE-004 UV holds below return threshold");
m[PE_UV] = 21.5f; /* 高于返回门槛:释放 */
prot_elm_eval(2200u, m);
prot_elm_state(PE_UV, &st);
CHECK((st.picked == 0u) && (st.asserted == 0u),
"TC-PE-004 UV above return threshold: drops out");
}
static void tc_disabled(void) {
float m[PE_COUNT];
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_OC2_EN, 0);
mk_base(m);
m[PE_OC2] = 999.0f;
prot_elm_eval(3000u, m);
prot_elm_state(PE_OC2, &st);
CHECK((st.enabled == 0u) && (st.picked == 0u) && (st.asserted == 0u),
"TC-PE-005 enable=0: no judgement even at 999x");
}
static void tc_set_lock(void) {
float m[PE_COUNT];
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
mk_base(m);
m[PE_OC1] = 10.1f;
(void)prot_set_set(PS_OC1_DLY, PROT_SET_Q10(0.0));
prot_elm_eval(4000u, m);
prot_elm_state(PE_OC1, &st);
CHECK(st.asserted != 0u, "TC-PE-006 pre: element operates while settings valid");
(void)prot_set_set(PS_OC1_CUR, PROT_SET_Q10(9.0)); /* 改值不封存 */
CHECK(prot_set_selfcheck() == -1, "TC-PE-006 tamper detected by selfcheck");
prot_elm_eval(4100u, m);
prot_elm_state(PE_OC1, &st);
CHECK((st.enabled == 0u) && (st.asserted == 0u),
"TC-PE-006 settings lock resets all elements (L5-1 gating)");
prot_set_seal();
prot_elm_eval(4200u, m);
prot_elm_state(PE_OC1, &st);
CHECK((st.enabled != 0u) && (st.asserted != 0u),
"TC-PE-006 unlock restores judgement");
}
static void tc_tick_wrap(void) {
float m[PE_COUNT];
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_OC2_DLY, PROT_SET_Q10(100.0));
mk_base(m);
m[PE_OC2] = 5.1f; /* OC2 定值 5.0 */
prot_elm_eval(0xFFFFFFF0u, m); /* 启动点贴近回绕 */
prot_elm_state(PE_OC2, &st);
CHECK(st.picked != 0u, "TC-PE-007 pickup near tick wrap");
prot_elm_eval(0x00000010u, m); /* 回绕后 32 ms */
prot_elm_state(PE_OC2, &st);
CHECK(st.asserted == 0u, "TC-PE-007 not asserted at 32ms after wrap");
prot_elm_eval(0x00000060u, m); /* 回绕后 112 ms */
prot_elm_state(PE_OC2, &st);
CHECK(st.asserted != 0u, "TC-PE-007 asserted at 112ms across wrap");
}
static void tc_active_mask(void) {
float m[PE_COUNT];
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_OC1_DLY, PROT_SET_Q10(0.0));
(void)prot_set_set(PS_OV_DLY, PROT_SET_Q10(0.0));
mk_base(m);
m[PE_OC1] = 10.1f;
m[PE_OV] = 130.0f; /* OV 定值 120 */
prot_elm_eval(6000u, m);
CHECK(prot_elm_active_mask() == ((1uL << PE_OC1) | (1uL << PE_OV)),
"TC-PE-008 active mask aggregates OC1+OV only");
}
static void tc_guards(void) {
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
prot_elm_state(PE_COUNT, &st);
CHECK((st.enabled == 0u) && (st.picked == 0u) && (st.asserted == 0u),
"TC-PE-009 out-of-range id -> zero state");
prot_elm_state(PE_OC1, NULL); /* 不得崩溃 */
CHECK(prot_elm_name(PE_COUNT) != NULL, "TC-PE-009 name out-of-range -> fallback");
CHECK(prot_elm_name(PE_3U0) != NULL, "TC-PE-009 name valid id -> non-NULL");
prot_elm_eval(7000u, NULL); /* NULL 测量:按 0 处理,不得崩溃 */
CHECK(1, "TC-PE-009 eval(NULL meas) survives");
}
int main(void) {
printf("=== Prot Elm Tests ===\n");
tc_init_clear();
tc_over_hysteresis();
tc_zero_delay();
tc_under_direction();
tc_disabled();
tc_set_lock();
tc_tick_wrap();
tc_active_mask();
tc_guards();
printf("\n=== Prot Elm Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""保护元件框架(L3,P1 子集)主机侧单元测试运行器(L0.5,纯标准库)。
做什么:用本机 MSVC 离线编译 test/host/prot_elm/prot_elm_test.c(#include 纳入
生产源文件 prot_elm.c + prot_set.c + phasor.c + sample_ring.c),验证
越限滞回 / 定时限 / 欠量方向 / 投退 / 定值闭锁联动 / 时基回绕 / 位图聚合。
用法(仓库根目录):
& '.venv\\Scripts\\python.exe' test/host/prot_elm/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误。
设计依据:docs/算法/继电保护功能复现规划.md §3.3(L3-1/3/4/6)、§8(验收判据)。
创建日期:2026-10-11
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
BUILD = TestBuild(
name="Prot Elm Tests",
src=os.path.join(HERE, "prot_elm_test.c"),
include_dirs=[
os.path.join(ROOT, "App", "algo"),
HERE,
],
summary_marker="=== Prot Elm Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+156
View File
@@ -0,0 +1,156 @@
/*
* 模块名称:保护定值模型(P1)主机侧单元测试
* 模块功能:在 PC 上对 App/algo/prot_set.c 做离线验证。
*
* 被测对象:真实源文件(#include 原样纳入)。纯逻辑,无需桩。
*
* 覆盖点(docs/算法/继电保护功能复现规划.md §5.2 / §3.4 L5-1):
* TC-PS-001 init:默认值装载、越界位图 0、selfcheck 通过、未闭锁
* TC-PS-002 set:范围内写入生效;越界拒写且原值不动
* TC-PS-003 校验和封存语义:set 后未 seal → selfcheck 失败并闭锁;
* seal → 通过并解锁(L5-1 核心闭环)
* TC-PS-004 load_raw:合法整表装载并封存;含越界项 → 整表不装载 +
* bad_mask 指认越界项(定值一致性:宁可全旧不可半新)
* TC-PS-005 Q10 往返精度(±1 LSB)
* TC-PS-006 元数据表自检:min<=max、投退项 0..1、非零通讯地址不重复
* TC-PS-007 越界编号守卫(get/set)
*
* 通过:全部 TC PASS(main 返回 0)。
* 创建日期:2026-10-11
*/
#include <stdint.h>
#include <stdio.h>
static int s_total = 0, s_passed = 0, s_failed = 0;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
} while (0)
#include "prot_set.c"
/* TC-PS-001 */
static void tc_init(void) {
prot_set_init();
CHECK(prot_set_bad_mask() == 0u, "TC-PS-001 init: no bad items");
CHECK(prot_set_selfcheck() == 0, "TC-PS-001 init: selfcheck pass");
CHECK(prot_set_locked() == 0u, "TC-PS-001 init: not locked");
CHECK(prot_set_get_f(PS_OC1_CUR) > 9.99f && prot_set_get_f(PS_OC1_CUR) < 10.01f,
"TC-PS-001 default OC1 current = 10.0 A");
CHECK(prot_set_get_ctl(PS_OC3_TRIP) == 0u,
"TC-PS-001 default OC3 = alarm (trip off)");
}
/* TC-PS-002 */
static void tc_set_range(void) {
prot_set_init();
CHECK(prot_set_set(PS_OC2_CUR, PROT_SET_Q10(7.5)) == 0,
"TC-PS-002 in-range set accepted");
CHECK(prot_set_get_f(PS_OC2_CUR) > 7.49f && prot_set_get_f(PS_OC2_CUR) < 7.51f,
"TC-PS-002 value readable after set");
CHECK(prot_set_set(PS_OC2_CUR, PROT_SET_Q10(101.0)) == -1,
"TC-PS-002 over-max set rejected");
CHECK(prot_set_get_f(PS_OC2_CUR) > 7.49f && prot_set_get_f(PS_OC2_CUR) < 7.51f,
"TC-PS-002 rejected set leaves value unchanged");
CHECK(prot_set_set(PS_OC1_EN, PROT_SET_Q10(2)) == -1,
"TC-PS-002 ctl item rejects value 2");
}
/* TC-PS-003 */
static void tc_seal_selfcheck(void) {
prot_set_init();
CHECK(prot_set_set(PS_OC1_CUR, PROT_SET_Q10(8.0)) == 0, "TC-PS-003 set ok");
CHECK(prot_set_selfcheck() == -1,
"TC-PS-003 set-without-seal => selfcheck FAIL (tamper detect)");
CHECK(prot_set_locked() == 1u, "TC-PS-003 selfcheck failure latches lock");
prot_set_seal();
CHECK(prot_set_selfcheck() == 0, "TC-PS-003 seal => selfcheck pass");
CHECK(prot_set_locked() == 0u, "TC-PS-003 seal clears lock");
}
/* TC-PS-004 */
static void tc_load_raw(void) {
int32_t tbl[PS_ID_COUNT];
uint32_t i;
prot_set_init();
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
tbl[i] = prot_set_get((prot_set_id_t)i);
}
tbl[PS_OL_CUR] = PROT_SET_Q10(1.5);
CHECK(prot_set_load_raw(tbl) == 0, "TC-PS-004 valid table loads");
CHECK(prot_set_get_f(PS_OL_CUR) > 1.49f && prot_set_get_f(PS_OL_CUR) < 1.51f,
"TC-PS-004 loaded value visible");
CHECK(prot_set_selfcheck() == 0, "TC-PS-004 load_raw auto-seals");
tbl[PS_OC3_DLY] = PROT_SET_Q10(60001.0); /* 越上限 */
tbl[PS_OL_CUR] = PROT_SET_Q10(2.5); /* 合法改动 */
CHECK(prot_set_load_raw(tbl) == -1, "TC-PS-004 bad table rejected");
CHECK(prot_set_bad_mask() == (1uL << PS_OC3_DLY),
"TC-PS-004 bad_mask points at the offending item");
CHECK(prot_set_get_f(PS_OL_CUR) > 1.49f && prot_set_get_f(PS_OL_CUR) < 1.51f,
"TC-PS-004 rejected load keeps OLD table (all-or-nothing)");
CHECK(prot_set_load_raw(0) == -1, "TC-PS-004 NULL table rejected");
}
/* TC-PS-005 */
static void tc_q10_roundtrip(void) {
float v = 12.345f;
int32_t q;
prot_set_init();
q = PROT_SET_Q10(v);
CHECK(prot_set_set(PS_OC1_CUR, q) == 0, "TC-PS-005 set ok");
{
float back = prot_set_get_f(PS_OC1_CUR);
float err = back - v;
if (err < 0.0f) { err = -err; }
CHECK(err < 0.001f, "TC-PS-005 Q10 roundtrip error < 1 LSB");
}
}
/* TC-PS-006 */
static void tc_meta_sanity(void) {
uint32_t i, j;
int ok_range = 1, ok_ctl = 1, ok_addr = 1;
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
const prot_set_meta_t *m = &g_prot_set_meta[i];
if (m->min > m->max) { ok_range = 0; }
if ((m->def < m->min) || (m->def > m->max)) { ok_range = 0; }
if ((m->is_ctl != 0u) &&
((m->min != 0) || (m->max != PROT_SET_Q10(1)))) { ok_ctl = 0; }
if (m->comm_addr == 0u) { continue; }
for (j = i + 1u; j < (uint32_t)PS_ID_COUNT; j++) {
if (g_prot_set_meta[j].comm_addr == m->comm_addr) { ok_addr = 0; }
}
}
CHECK(ok_range, "TC-PS-006 meta: min<=def<=max for all items");
CHECK(ok_ctl, "TC-PS-006 meta: ctl items are 0..1");
CHECK(ok_addr, "TC-PS-006 meta: comm addresses unique");
CHECK((uint32_t)PS_ID_COUNT <= 32u, "TC-PS-006 item count fits bad_mask bitmap");
}
/* TC-PS-007 */
static void tc_id_guard(void) {
prot_set_init();
CHECK(prot_set_get(PS_ID_COUNT) == 0, "TC-PS-007 get out-of-range id -> 0");
CHECK(prot_set_set(PS_ID_COUNT, 0) == -1, "TC-PS-007 set out-of-range id -> -1");
}
int main(void) {
printf("=== Prot Set Tests ===\n");
tc_init();
tc_set_range();
tc_seal_selfcheck();
tc_load_raw();
tc_q10_roundtrip();
tc_meta_sanity();
tc_id_guard();
printf("\n=== Prot Set Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""保护定值模型(P1)主机侧单元测试运行器(L0.5,纯标准库)。
做什么:用本机 MSVC 离线编译 test/host/prot_set/prot_set_test.c(#include 纳入
生产源文件 App/algo/prot_set.c),验证 Q10 定点模型 / 范围校验 /
整表装载 all-or-nothing / 校验和封存-自检-闭锁(L5-1)闭环。
用法(仓库根目录):
& '.venv\\Scripts\\python.exe' test/host/prot_set/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误。
设计依据:docs/算法/继电保护功能复现规划.md §5(定值参数模型)、§3.4(L5-1)。
创建日期:2026-10-11
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
BUILD = TestBuild(
name="Prot Set Tests",
src=os.path.join(HERE, "prot_set_test.c"),
include_dirs=[
os.path.join(ROOT, "App", "algo"),
HERE,
],
summary_marker="=== Prot Set Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))