diff --git a/.cubemx_survival_baseline.json b/.cubemx_survival_baseline.json index bea7223d..fa05f149 100644 --- a/.cubemx_survival_baseline.json +++ b/.cubemx_survival_baseline.json @@ -5,6 +5,7 @@ "fault-handlers-3": "f3afab8d2be18794", "fault-handler-enable": "be87fcd3a7926138", "hardfault-report-impl": "f3afab8d2be18794", + "error-handler-degrade": "d02a92afda945c4b", "stack-overflow-check": "d4735e3a265e16ee", "malloc-failed-hook": "6b86b273ff34fce1", "assert-not-deadloop": "d4a00718ab2bd071", @@ -12,14 +13,21 @@ "stack-overflow-hook-impl": "a006edbd30d2c4d7", "malloc-failed-hook-impl": "fcd7250e5cd6403b", "stack-overflow-hook-helper": "cacd9fd028be9b5b", - "watchdog-module": "3d908285b092c402", + "watchdog-module": "663f05caf22cf7d0", "reset-cause-latch": "52fc71acbb0f1aed", "iwdg-hal-driver": "4b902875a84554e9", "hal-iwdg-enabled": "1a8ed6cff34f7ba6", "ioc-keepusercode": "b5bea41b6c623f7c", "ioc-iwdg-present": "d3a6e667048c95db", "ioc-task-stacks": "3262f737d92dcbee", - "adc1-hal-enabled": "1a8ed6cff34f7ba6" + "adc1-hal-enabled": "1a8ed6cff34f7ba6", + "adc1-calibration-call": "8d5f4b40fb218913", + "adc1-calibration-impl": "d33bc4323e243248", + "adc-tick-tim3-branch": "39c3548c37f6c4ae", + "ioc-adc-trig-t3trgo": "a9939de260ee46f8", + "ioc-adc-dma-continuous": "189126670d85d80a", + "adc1-dma-link": "397fce92dca3b728", + "adc1-dma-irq-priority": "beff08e91333229b" }, "_note": "CubeMX 存活审计基线;改动用 --write 重建" } diff --git a/.dsh/skills/keil-build/SKILL.md b/.dsh/skills/keil-build/SKILL.md index 048386a7..a84e07b2 100644 --- a/.dsh/skills/keil-build/SKILL.md +++ b/.dsh/skills/keil-build/SKILL.md @@ -35,9 +35,11 @@ whenToUse: 需要编译 Keil MDK 工程、验证代码改动是否 0 错误 0 硬件闭环只会把编译错误伪装成运行问题。 - **警告也算失败**:判 PASS 只认退出码 0,别只看"有没有 error:"。 - **`MDK-ARM/*.uvprojx` 每次构建后被 UV4 自动改写,改动一律不入库**:UV4 会把工程 - XML 归一化成本机风格(`` → `` 等)并把 `PackID` 降到本机 DFP 版本。 + XML 归一化成本机风格(`` → ``、XML 声明加 `standalone="no"`)并把 + `PackID` 降到**本机装的 DFP 版本**(属降级——提交后会把别人的 Pack 版本一起改掉)。 判断:`git diff` 里只见 XML 格式/`PackID` 差异而**无源文件挂载增删** → `git checkout -- ` 丢弃;只有真的增删了源文件(grep 文件名计数变化)才提交。 + 同目录的 `*.uvoptx` / `*.uvguix.*` 是 IDE 个人设置,已在 `.gitignore` 里,不会露出来。 - 报错定位读日志别只看终端回显:本仓库 = `MDK-ARM\build_log.txt`。 - **exit 2 先分清"真没设"还是"会话没继承"**(2026-10-10 实测):agent 的 shell 由 常驻服务派生,服务启动**之后**设置的用户级环境变量不会自动进来。查 diff --git a/.dsh/skills/lvgl-simulator/SKILL.md b/.dsh/skills/lvgl-simulator/SKILL.md new file mode 100644 index 00000000..f4cb2e9b --- /dev/null +++ b/.dsh/skills/lvgl-simulator/SKILL.md @@ -0,0 +1,52 @@ +--- +name: lvgl-simulator +description: LVGL PC 模拟器构建/运行/自检入口:build_sim.bat 自动探测本机 VS、lvgl_sim.exe 无头截图与自检开关(--screenshot/--keys/--standby/--cfgtest/--logtest/--idle)、三套回归脚本、两份 lv_conf.h 同步门禁、构建报错速查(SDL2/lv_conf 找不到、全黑、键盘无响应)。 +whenToUse: 需要构建或运行模拟器、无头截图出图、跑模拟器自检或 UI 回归、修改 simulator/ 目录代码或 simulator/lv_conf.h、排查模拟器构建/运行异常时。UI 改动后的完整回归走 ui-verify skill。 +--- + +# LVGL PC 模拟器(simulator/) + +PC 上编译运行与 MCU **同一份** UI 源码(CMake + MSVC + SDL2,x64,160×160 I1 单色),免烧录。 +**目录架构 / 完整坑与 FAQ → `simulator/README.md`;首次部署(SDL2 SDK)→ `docs/UI设计/LVGL模拟器部署与使用说明.md`。本 skill 只放操作入口。** + +## 构建与运行(仓库根执行) + +```powershell +& "simulator\build_sim.bat" # 增量构建;vswhere 自动探测本机 VS(16/17/18),勿写死生成器 +& "simulator\build_sim.bat" -Clean # 全量重建(遇到奇怪问题先加它) +.\simulator\build\Debug\lvgl_sim.exe # 窗口模式:↑↓←→ / Enter / Esc(纯键盘,无鼠标) +``` + +⚠ 窗口模式默认 **60 s 空闲自动退出**(防挂后台),`--idle <秒>` 覆盖。 + +## 无头自检(退出码 0=PASS / 非 0=FAIL) + +| 命令(在 `simulator\build\Debug\` 下或用全路径) | 用途 | +|---|---| +| `lvgl_sim.exe --screenshot shot.bin` | 离屏渲染首帧 → `simulator/i1_to_png.py` 转 PNG 目检 | +| `lvgl_sim.exe --keys "ud" shot.bin` | 注入键序后截图:`u`=↑ `d`=↓ `l`=← `r`=→ `e`=Enter `s`=Esc(`main.c` 实锤) | +| `lvgl_sim.exe --standby shot.bin` | 待机页截图 | +| `lvgl_sim.exe --cfgtest` | 配置引擎闭环:加载→轮询→外部改文件重载→外科式保存回验(在当前 CWD 产生 `sim_config.ini`) | +| `lvgl_sim.exe --logtest` | syslog 后端闭环(定长记录/时间戳/轮转) | + +## UI 改动回归 → 用 `ui-verify` skill(勿在此零散手跑) + +5 步门禁 = 构建 → `check_menu_rows.py`(菜单 8 状态像素 + 池基线)→ `check_content_pages.py`(文本断言 + 11 页黄金参考 + 往返池)→ `@build` → `coding-standard`。 +RemoDispBus 推流回归单独跑:`.venv\Scripts\python.exe simulator/tests/check_remo_sink.py`。 +黄金参考 `tests/ref/` 是**本地生成物不入库**:换机后先 `check_content_pages.py --update` 生成,再 `regen_ref_png.py` 出目检 PNG。 + +## 两条铁律 + +- **改 `simulator/lv_conf.h` 触发 `lv-conf-sync` 门禁**:它与 MCU 端 `Lib/lv_conf.h` 必须保持同一特性子集(按**生效值**比对,未定义取 `lv_conf_internal.h` 兜底);白名单与四类判定 → `docs/代码规范/pre-commit使用说明.md` §2.15 / §5.5 +- **模拟器代码与固件同标准受检**(`cppcheck` / `coding-standard` / 乱码 / 改写型钩子 / `doc-links` 全覆盖 `simulator/`);**改动触及 `sim/` 任何后端,必须同步在 `docs/测试说明/模拟器差距清单.md` 登记一行**——模拟器跑过 ≠ 真机跑过 + +## 构建/运行报错速查(详解 `simulator/README.md`「四、已知坑(构建/运行)」) + +| 现象 | 根因 | +|---|---| +| 找不到 `SDL2/SDL.h` | LVGL SDL 驱动写死该路径,头文件须有 `include/SDL2/` 副本(仓库已内置) | +| 找不到 `lv_conf.h` | `LV_CONF_DIR`/`SDL2_INCLUDE_DIR` 必须在 `add_subdirectory(lvgl)` **之前**定义 | +| 窗口全黑 | mono 主题未生效 / 缺 `lv_refr_now(NULL)` / 重复喂 tick(SDL 驱动内部已 `lv_tick_set_cb`) | +| 画面正常但按键全无效 | 主循环**不得**调 `SDL_PollEvent`——LVGL SDL 驱动内部 5ms 定时器统一分发,先轮询会抢光事件 | +| 中文显示空白 | 子集字体缺字 → **`font-regen` skill** 重生成 `ui_font_cn_12` | +| 新加 UI `.c` 一端编不过 | 双端注册(uvprojx + CMakeLists)→ **`new-source-file` skill** | diff --git a/.dsh/skills/new-machine-setup/SKILL.md b/.dsh/skills/new-machine-setup/SKILL.md index b5b73ed0..72f28007 100644 --- a/.dsh/skills/new-machine-setup/SKILL.md +++ b/.dsh/skills/new-machine-setup/SKILL.md @@ -46,7 +46,7 @@ whenToUse: 换新机器/新克隆要把环境搭起来、构建或烧录报"环 (`env_check.py` 对这种情况报 WARN 并给注入命令。) - 换机后 COM 口几乎必变:只改 `.serialtest/config.json` 的 `portName`, 然后 `serial_tool.py restart`(daemon 启动时缓存配置)。 -- 串口"拒绝访问"先查残留旧 daemon(AGENTS.md 串口助手节),不是驱动问题。 +- 串口"拒绝访问"先查残留旧 daemon(`serial-assistant` skill「陷阱」节),不是驱动问题。 ## 其它项目怎么套 diff --git a/.dsh/skills/task-worktree/.gitignore b/.dsh/skills/task-worktree/.gitignore new file mode 100644 index 00000000..c18dd8d8 --- /dev/null +++ b/.dsh/skills/task-worktree/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/.dsh/skills/task-worktree/SKILL.md b/.dsh/skills/task-worktree/SKILL.md new file mode 100644 index 00000000..b8c53f84 --- /dev/null +++ b/.dsh/skills/task-worktree/SKILL.md @@ -0,0 +1,111 @@ +--- +name: task-worktree +description: 多任务并行开发的空间管理(git worktree 生命周期):new 开一棵工作树 + 任务分支、目录联接共享 .venv 与模拟器 SDK、build/bench 两类独占锁、done 预演合并回主干并审计并集残留。规格见 docs/环境与部署/并行任务工作树方案.md。 +whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;要为新任务开一个隔离空间;要把任务合并回主干;或遇到两个任务抢 COM6 / UV4、"改了文档被覆盖"、并集合并残留时。 +--- + +# task-worktree — 多任务并行开发空间(短命令 `@task`) + +**执行体 = 本 skill 自带脚本** `.dsh/skills/task-worktree/scripts/task_worktree.py` +(纯标准库,任意 python3;拷整个 skill 目录到别的项目即可用)。 + +```powershell +# cwd = 仓库根(或任意子目录) +& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" <子命令> [参数] +``` + +退出码:`0`=成功 / `1`=操作失败 / `2`=前置或环境错误 / `3`=用法。 + +## 固定命令 + +| 命令 | 作用 | +|---|---| +| `new ` | 建分支 `task/-` + 工作树 + 共享联接 + 台账;打印新空间路径 | +| `list` | 所有任务一览:分支 / 落后领先 / 脏文件数 / 台架状态 + 两个锁的归属 | +| `status ` | 单任务详情(未提交清单、相对基线漂移、台架记录、已建联接) | +| `sync ` | 在任务工作树里 `rebase` 追平主干,把冲突控制在任务分支上 | +| `lock acquire\|release\|status ` | 独占资源锁 | +| `done ` | 预演 → 合入主干 → 并集残留审计 → 重生成产物 → 门禁 → 清场 | +| `abort ` | 放弃任务(工作树 + 分支一起删,**永久丢弃**) | + +`new` 常用开关:`--base <分支>`(默认 `v2-on-ui`)· `--needs-bench`(无台架 PASS 则 `done` 拒合)· +`--lean`(跳过 Git LFS smudge)· `--no-bootstrap`(不建联接)。 + +`done` 常用开关:`--dry-run`(只预演,零改动)· `--skip-build` · `--skip-gates` · `--force`。 + +**环境变量**(高级):`DTU_WT_ROOT`(任务容器位置,默认仓库同级 `_wt/`)· +`DTU_TASK_BASE`(默认基线分支,默认 `v2-on-ui`)· +`DTU_INTEGRATION_WORKTREE`(集成点,默认主工作树——用于隔离演练/测试)。 + +## 典型流程 + +```powershell +& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" new N21 dhara-isfree --needs-bench +# → 把新会话的工作目录切到打印出来的 _wt/N21-dhara-isfree/,在那里干活 + +& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" lock acquire bench --task N21 +# 烧录 / @test / 板级套件…… +& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" lock release bench --task N21 --bench-pass + +# 回到主工作树执行回收 +& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" done N21 +``` + +## 铁律 + +1. **`done` 只能在主工作树执行**——主干分支检出在主工作树,git 不允许同一分支在两处检出。 +2. **`done` 不 push**。它结束于本地合入 + 门禁全绿;推送需用户明确许可(项目既有纪律)。 +3. **`build` 与 `bench` 都必须持锁**:UV4 不能两个实例同时 `-b`;串口 COM6、ST-LINK、 + 设备 IP `192.168.1.100` 各只有一份。**构建与烧录都不可并发。** +4. **单写者文档**:`docs/任务规划/`(整个文件夹)、`AGENTS.md` 的任务表块、`MDK-ARM/*.uvprojx`、 + `.pre-commit-config.yaml`、内存快照 —— 任务期**不在任务工作树里改**,留到 `done` 统一处理。 +5. **清场必须先摘联接**:脚本会先 `os.rmdir` 掉 `.venv` / `simulator/SDL2` 联接再 + `git worktree remove`。**手工清场时千万别直接 `git worktree remove`**——git 可能递归进联接, + 把主工作树的 `.venv` 一起删掉。 +6. **并发上限 3 棵工作树**(每棵约 83 MB 检出)。 +7. **工作树放仓库同级 `_wt/`,绝不在仓库内部**——否则污染 `git status`、`pre-commit --all-files` 与全库搜索。 + +## 陷阱(实测) + +- **联接不是符号链接**:本仓库 `core.symlinks=false`,git 里的 symlink 会被检出成 + 「内容是目标路径的文本文件」。共享 `.venv` 只能用 Windows **目录联接**(`mklink /J`, + 不需要管理员权限),且联接必须建在版本控制之外。 + 实测(2026-10-12):用 ctypes 直调 `DeviceIoControl(FSCTL_SET_REPARSE_POINT)` + 会被系统拒绝(Win32 4392),`mklink /J` 正常——所以脚本走后者,参数以列表交给 + subprocess,含空格的路径(本项目 `DTU Code`)由 subprocess 自动加引号。 +- **联接建失败不中止 `new`**:只告警并打印手工补建命令,避免留下半成品任务空间; + 但该任务里跑不了 `.venv\Scripts\python.exe`,`@build` / `ci_local.py` 会找不到解释器。 +- **`done` 前主工作树必须干净**:否则未提交改动会被卷进合并。 +- **⚠ `done` 的门禁跑在 `@build` 之前 → 依赖 `.map` 的钩子会误报**(2026-10-12 实测): + `done` 的顺序是「合并 → 重生成 → **门禁(ci_local)** → `@build`」,而 + `mem-budget`(N-27)/ `mem-numbers` 是拿**文档快照 vs `.map`** 比对 —— 门禁跑时 + `.map` 还是**主工作树合并前**的旧产物,与刚合并进来的新台账**必然不一致** → + 误报 `[FAIL] 台账受管区与 .map 不一致`。`@build` 跑完后 `.map` 才是新的。 + **处置**:门禁失败且只差这两项时,按提示在主工作树跑 + `python scripts/ram_baseline.py --write` + `python scripts/check_mem_numbers.py --write` + 后追加提交,再重跑 `done`(它会检测到「已合入」直接清场)。 +- **⚠ 增量构建可能不重链 → `.map` 数字瞬态不一致**(2026-10-12 实测): + `@build` 若判定无需重链,`.map` 保持旧值,此时 `--write` 生成的快照是**瞬态值** + (实测同一份源码两次读到 223,560 与 223,724,差 164 B)。**以主工作树(集成点) + 的 `.map` 为准**;改完 RAM 相关代码后确认重链再刷新快照。 +- **合并冲突常落在「生成物」上**:`docs/内存管理/RAM使用分析与优化方案.md`(mem-numbers + 快照)在双方都改过时必冲突,且**两边都不能直接选**(各自基于不同基线)—— + 正确做法是解冲突后用**合并后的实际构建**重新生成,不要挑一侧。 +- **并集合并残留**:双方都改过的文件(尤其 `.gitignore`)会被 git 自动并成**并集**, + 可能留下指向已删目录的死规则或重复条目。脚本会自动报「双方都改过」的文件清单 + + 连续重复行;**权威判据是 base / 两个 tip 三方对比**(`git show :` 逐项 `grep -F`), + 别用 `tail -N` 目测。 +- **`.workbuddy-ai/` 刻意不共享**:记忆不随任务分叉,也不给联接递归删除的机会。 + 任务工作树自己的记忆由 `done` / `abort` 在清场前归并回主工作树。 +- **`--lean` 只省磁盘不省时间**:实测全量 smudge 7.25 s / 83 MB,跳过 smudge 7.05 s / 45 MB, + 两者工作区都干净。默认走全量(参考图可用),磁盘紧张时才用 `--lean`。 +- **锁不绑定进程存活**:`lock acquire` 本身是短命进程,而真正用资源的烧录/构建在别的进程里跑。 + 所以判「陈旧」只看**时效**(默认 6 小时,`LOCK_TTL_HOURS`)加显式 `--force`;pid 仅作排障记录。 + 同任务重复 acquire 是幂等的(不会把自己锁死)。 +- **陈旧锁可自动接管**:超过 6 小时未释放即视为陈旧,下次 acquire 会接管并提示;活锁需 `--force`。 + +## 与其它 skill 的衔接 + +- 构建 = `keil-build` skill(`@build`);烧录/回读 = `stlink-flash` skill;串口闭环 = `serial-boot-test` skill +- 新 `.c` 文件 = `new-source-file` skill(`uvprojx` 与 `CMakeLists.txt` 双端注册) +- 完整机制与目录分配 → `docs/环境与部署/并行任务工作树方案.md` diff --git a/.dsh/skills/task-worktree/scripts/task_worktree.py b/.dsh/skills/task-worktree/scripts/task_worktree.py new file mode 100644 index 00000000..7a7ab99c --- /dev/null +++ b/.dsh/skills/task-worktree/scripts/task_worktree.py @@ -0,0 +1,857 @@ +#!/usr/bin/env python3 +"""task-worktree — 多任务并行开发的空间管理(git worktree 生命周期) + +执行体 = 本脚本(纯标准库)。规格见 docs/环境与部署/并行任务工作树方案.md, +用法与铁律见同目录 SKILL.md。 + +退出码:0=成功 / 1=操作失败 / 2=前置或环境错误 / 3=用法错误 +""" + +import argparse +import json +import os +import re +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +# 输出统一 UTF-8,避免被控制台代码页(cp936)转码破坏 +for _stream in (sys.stdout, sys.stderr): + try: + _stream.reconfigure(encoding="utf-8") + except Exception: + pass + +EXIT_OK, EXIT_FAIL, EXIT_ENV, EXIT_USAGE = 0, 1, 2, 3 + +BRANCH_PREFIX = "task/" +MAX_CONCURRENT = 3 +LOCK_NAMES = ("build", "bench") +LOCK_TTL_HOURS = 6.0 # 超过此持有时间即视为陈旧锁,下次 acquire 自动接管 +JUNCTION_SPECS = ( + (".venv", "Python 环境"), + ("simulator/SDL2", "模拟器 SDL2 SDK"), +) +FILE_ATTRIBUTE_REPARSE_POINT = 0x400 + +ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$") +SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$") + + +class Fail(Exception): + """操作失败(退出码 1)""" + + +class EnvError(Exception): + """前置或环境错误(退出码 2)""" + + +# --------------------------------------------------------------------- 仓库定位 + + +def _main_worktree_of(start: Path): + """借 git 反查「主工作树」路径——脚本可能被从任务工作树里调用,不能只靠 __file__""" + for extra in (["--path-format=absolute"], []): + r = subprocess.run( + ["git", "-C", str(start), "rev-parse", *extra, "--git-common-dir"], + capture_output=True, text=True, encoding="utf-8", errors="replace", + stdin=subprocess.DEVNULL, + ) + if r.returncode == 0 and r.stdout.strip(): + common = Path(r.stdout.strip()) + if not common.is_absolute(): + common = (start / common).resolve() + return common.parent if common.name == ".git" else common + return None + + +_SCRIPT_ROOT = Path(__file__).resolve().parents[4] +REPO_ROOT = _main_worktree_of(_SCRIPT_ROOT) or _SCRIPT_ROOT +WT_ROOT = Path(os.environ.get("DTU_WT_ROOT") or (REPO_ROOT.parent / "_wt")) +BASE_BRANCH = os.environ.get("DTU_TASK_BASE", "v2-on-ui") + +STATE_DIR = WT_ROOT / ".state" +LOCKS_DIR = STATE_DIR / "locks" + + +# --------------------------------------------------------------------- 基础工具 + + +def git(*args, cwd=None, check=True, env=None): + cmd = ["git"] + [str(a) for a in args] + full_env = dict(os.environ) + if env: + full_env.update(env) + r = subprocess.run( + cmd, cwd=str(cwd or REPO_ROOT), capture_output=True, text=True, + encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL, env=full_env, + ) + if check and r.returncode != 0: + raise Fail("git {} 失败(rc={})\n{}".format( + " ".join(cmd[1:]), r.returncode, (r.stdout + r.stderr).strip())) + return r + + +def git_out(*args, **kw): + return git(*args, **kw).stdout.strip() + + +def worktree_entries(): + out = git_out("worktree", "list", "--porcelain") + entries, cur = [], None + for line in out.splitlines(): + if line.startswith("worktree "): + if cur: + entries.append(cur) + cur = {"path": Path(line[len("worktree "):]), "branch": None} + elif cur is None: + continue + elif line.startswith("branch refs/heads/"): + cur["branch"] = line[len("branch refs/heads/"):] + if cur: + entries.append(cur) + return entries + + +def main_worktree() -> Path: + """集成点(合并发生的地方)。默认 = 主工作树;可用环境变量覆盖,便于演练/测试。""" + override = os.environ.get("DTU_INTEGRATION_WORKTREE") + if override: + p = Path(override) + if not p.is_absolute(): + p = (REPO_ROOT / p).resolve() + if not p.is_dir(): + raise EnvError("DTU_INTEGRATION_WORKTREE 不存在:{}".format(p)) + return p + entries = worktree_entries() + if not entries: + raise EnvError("无法解析 git worktree 列表") + return entries[0]["path"] + + +def venv_python(tree: Path = None) -> Path: + """优先用该工作树自己的 .venv(任务树里是指向主树的联接),否则退回主工作树""" + for root in (tree, REPO_ROOT): + if root is None: + continue + p = Path(root) / ".venv" / "Scripts" / "python.exe" + if p.is_file(): + return p + return Path(sys.executable) + + +def run_script(script: str, args, cwd: Path, label: str, tail: int = 40) -> bool: + """跑项目脚本并回报。输出可能上千行,只回显尾部(结论与报错都在尾部) + + ⚠ 脚本必须取自 **cwd 那棵树**,不能固定用 REPO_ROOT: + 项目脚本普遍以 `__file__` 反推仓库根(如 `scripts/ci_local.py`、 + `scripts/check_registration.py`),若拿主树的脚本、却给集成树的 cwd, + 脚本仍会去校验主树——门禁就测错了对象(2026-10-12 实测踩到: + 报出主树才有的 iec104 未注册缺口,而集成树里根本没那些文件)。 + """ + script_path = Path(cwd) / script + if not script_path.is_file(): + script_path = REPO_ROOT / script + cmd = [str(venv_python(Path(cwd))), str(script_path)] + [str(x) for x in args] + r = subprocess.run( + cmd, cwd=str(cwd), capture_output=True, text=True, + encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL, + ) + out = (r.stdout + r.stderr).strip() + print("[{}] {}".format("ok" if r.returncode == 0 else "!!", label)) + if out: + lines = out.splitlines() + if len(lines) > tail: + print(" …(前 {} 行略)".format(len(lines) - tail)) + lines = lines[-tail:] + print(" " + "\n ".join(lines)) + return r.returncode == 0 + + +# --------------------------------------------------------------------- 台账 + + +def state_file(task_id: str) -> Path: + return STATE_DIR / (task_id + ".json") + + +def load_state(task_id: str) -> dict: + p = state_file(task_id) + if not p.is_file(): + raise EnvError("任务 {} 不存在(缺台账 {})".format(task_id, p)) + return json.loads(p.read_text(encoding="utf-8")) + + +def save_state(st: dict) -> None: + STATE_DIR.mkdir(parents=True, exist_ok=True) + state_file(st["id"]).write_text( + json.dumps(st, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +def all_states(): + if not STATE_DIR.is_dir(): + return [] + out = [] + for p in sorted(STATE_DIR.glob("*.json")): + try: + out.append(json.loads(p.read_text(encoding="utf-8"))) + except Exception: + continue + return out + + +# --------------------------------------------------------------------- 独占锁 + + +def lock_age_hours(info) -> float: + try: + return max(0.0, (time.time() - float(info.get("epoch") or 0)) / 3600.0) + except (TypeError, ValueError): + return float("inf") + + +def lock_path(name: str) -> Path: + return LOCKS_DIR / (name + ".lock") + + +def lock_read(name: str): + p = lock_path(name) + if not p.is_file(): + return None + try: + return json.loads(p.read_text(encoding="utf-8")) + except Exception: + return {"task": "?", "pid": 0, "epoch": 0, "at": "?"} + + +def lock_acquire(name: str, task: str, wait: int = 0, force: bool = False, note: str = ""): + """抢占独占资源。 + + ⚠ 锁**不**绑定进程存活:`lock acquire` 本身是短命进程,而用锁的烧录/构建 + 在另一个进程里跑。因此判「陈旧」只看**时效**(超过 LOCK_TTL_HOURS), + 外加显式 `--force`;pid 仅作记录便于排障。 + """ + LOCKS_DIR.mkdir(parents=True, exist_ok=True) + p = lock_path(name) + deadline = time.time() + max(0, wait) + while True: + try: + fd = os.open(str(p), os.O_CREAT | os.O_EXCL | os.O_WRONLY) + except FileExistsError: + info = lock_read(name) or {} + if info.get("task") == task: + return True, info + age = lock_age_hours(info) + if force or age > LOCK_TTL_HOURS: + print("[lock] 接管 {} 锁(原持有者 {},已持有 {:.1f} 小时{})".format( + name, info.get("task"), age, ",--force" if force else ",视为陈旧")) + try: + p.unlink() + except OSError: + pass + continue + if time.time() >= deadline: + return False, info + print("[lock] {} 被 {} 占用(已持有 {:.1f} 小时),等待…".format( + name, info.get("task"), age)) + time.sleep(2) + else: + now = time.time() + payload = { + "name": name, "task": task, "pid": os.getpid(), + "host": os.environ.get("COMPUTERNAME", "?"), + "epoch": now, + "at": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(now)), + "note": note, + } + os.write(fd, (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8")) + os.close(fd) + return True, payload + + +def lock_release(name: str, task=None, force: bool = False): + p = lock_path(name) + if not p.is_file(): + return True, "锁 {} 未被持有".format(name) + info = lock_read(name) or {} + if not force and task and info.get("task") not in (task, None): + return False, "锁 {} 由 {} 持有,与 {} 不符;强制释放请加 --force".format( + name, info.get("task"), task) + try: + p.unlink() + except OSError as e: + return False, "释放锁 {} 失败:{}".format(name, e) + return True, "已释放锁 {}(原持有者 {})".format(name, info.get("task")) + + +# --------------------------------------------------------------------- 目录联接 + + +def is_reparse(p: Path) -> bool: + try: + st = os.lstat(str(p)) + except OSError: + return False + return bool(getattr(st, "st_file_attributes", 0) & FILE_ATTRIBUTE_REPARSE_POINT) + + +def make_junction(link: Path, target: Path) -> None: + """创建 Windows 目录联接(不需要管理员权限,也不是符号链接) + + 走 `mklink /J`——这是唯一不需要 SeCreateSymbolicLinkPrivilege 的目录链接形式。 + ⚠ 实测(2026-10-12):直接用 ctypes 调 `DeviceIoControl(FSCTL_SET_REPARSE_POINT)` + 会被系统拒绝(Win32 4392 / ERROR_REPARSE_TAG_INVALID,缓冲区经 hexdump 核对无误), + 而 `mklink /J` 正常,故采用后者。参数以**列表**交给 subprocess, + 含空格的路径(本项目 `DTU Code`)由 subprocess 自动加引号,不经手写引号。 + """ + if os.name != "nt": + raise EnvError("目录联接是 Windows 专属能力;当前平台 {}".format(os.name)) + if os.path.lexists(str(link)): + raise Fail("联接位置已被占用:{}".format(link)) + if not target.exists(): + raise Fail("联接源不存在:{}".format(target)) + link.parent.mkdir(parents=True, exist_ok=True) + r = subprocess.run( + ["cmd", "/c", "mklink", "/J", str(link), str(target)], + capture_output=True, stdin=subprocess.DEVNULL, + ) + if r.returncode != 0 or not is_reparse(link): + detail = (r.stdout or b"").decode("mbcs", "replace").strip() + raise Fail("创建目录联接失败:{} -> {}\n rc={} {}".format( + link, target, r.returncode, detail)) + + +def remove_junction(link: Path) -> bool: + """只摘掉联接本身,绝不触碰目标内容(os.rmdir = RemoveDirectoryW)""" + if not is_reparse(link): + return False + try: + os.rmdir(str(link)) + return True + except OSError as e: + print("[warn] 摘除联接失败 {}:{}".format(link, e)) + return False + + +def bootstrap(wt: Path): + """建立共享联接。单个联接失败只告警不中止——避免留下半成品任务空间""" + made, failed = [], [] + for rel, desc in JUNCTION_SPECS: + target = REPO_ROOT / rel + if not target.exists(): + print("[skip] {}({})不存在,跳过联接".format(rel, desc)) + continue + try: + make_junction(wt / rel, target) + except (Fail, EnvError) as e: + failed.append(rel) + print("[WARN] {} 联接失败:{}".format(rel, e)) + continue + made.append(rel) + print("[ok] 联接 {} -> {}".format(rel, target)) + if failed: + print() + print("!! {} 个共享联接未建成;该任务里跑 `.venv\\Scripts\\python.exe` 会找不到解释器。".format(len(failed))) + for rel in failed: + print(" 手工补:cmd /c mklink /J \"{}\\{}\" \"{}\\{}\"".format(wt, rel, REPO_ROOT, rel)) + return made + + +# --------------------------------------------------------------------- 子进程脚本 + + +def regen(cwd: Path) -> None: + run_script("scripts/gen_task_table.py", ["--write"], cwd, "重生成 FreeRTOS 任务表") + if list((Path(cwd) / "MDK-ARM").glob("**/*.map")): + run_script("scripts/check_mem_numbers.py", ["--write"], cwd, "同步内存/Flash 快照") + else: + print("[skip] 未找到 .map,跳过内存快照同步(需先 @build)") + + +def run_gates(cwd: Path, skip_build: bool) -> bool: + ok = run_script("scripts/ci_local.py", [], cwd, "L0 + L0.5 本地等价 CI") + if skip_build: + print("[skip] 按 --skip-build 跳过 @build") + return ok + got, info = lock_acquire("build", "__done__", wait=900) + if not got: + print("!! build 锁被 {} 占用(UV4 不能并发构建),跳过 @build".format(info.get("task"))) + return False + try: + if not run_script(".dsh/skills/keil-build/scripts/keil_build.py", [], cwd, + "@build(0 错 0 警)"): + ok = False + finally: + lock_release("build", "__done__", force=True) + return ok + + +# --------------------------------------------------------------------- 合并辅助 + + +def write_merge_message(st: dict, base: str, cwd: Path) -> Path: + raw = git_out("log", "--format=%s", "{}..{}".format(base, st["branch"]), cwd=cwd) + subjects = [s.strip() for s in raw.splitlines() if s.strip()] + title = "merge: {} 合入 {}".format(st["branch"], base) + if len(title) > 100: + title = title[:97] + "..." + lines = [title, "", "任务 {}({} 个提交)经 task-worktree 回收:".format(st["id"], len(subjects))] + for s in subjects: + lines.append("- " + (s[:73] + "..." if len(s) > 76 else s)) + if st.get("note"): + note = st["note"] + lines.append("") + lines.append("备注:" + (note[:73] + "..." if len(note) > 76 else note)) + fd, path = tempfile.mkstemp(prefix="taskwt-merge-", suffix=".txt") + os.close(fd) + Path(path).write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n") + return Path(path) + + +def audit_residue(base_before: str, branch: str, cwd: Path) -> None: + """并集合并残留审计:双方都改过的文件会被 git 自动并成并集""" + mb = git_out("merge-base", base_before, branch, cwd=cwd) + task_files = set(filter(None, git_out("diff", "--name-only", mb, branch, cwd=cwd).splitlines())) + base_files = set(filter(None, git_out("diff", "--name-only", mb, base_before, cwd=cwd).splitlines())) + both = sorted(task_files & base_files) + if not both: + print("[ok] 没有「双方都改过」的文件,无并集残留风险") + return + print("以下 {} 个文件双方都改过,git 会自动并成并集,需人工核对:".format(len(both))) + for f in both[:30]: + print(" - " + f) + if len(both) > 30: + print(" …(其余 {} 个略)".format(len(both) - 30)) + hits = [] + for f in both: + p = cwd / f + if not p.is_file(): + continue + try: + lines = p.read_text(encoding="utf-8", errors="replace").splitlines() + except OSError: + continue + dups = [i + 1 for i in range(1, len(lines)) + if lines[i] == lines[i - 1] and lines[i].strip()] + if dups: + hits.append((f, dups)) + if hits: + print() + print("!! 连续重复行(并集合并残留嫌疑):") + for f, dups in hits: + print(" - {} 第 {} 行".format(f, ", ".join(str(d) for d in dups[:8]))) + else: + print("[ok] 未检测到连续重复行") + print("提示:权威判据是 base / 两个 tip 三方对比(git show :),别只看结果文件。") + + +def harvest_memory(st: dict) -> None: + """任务工作树会被删除,先把它自己的记忆归并回主工作树,避免丢上下文""" + src = Path(st["path"]) / ".workbuddy-ai" / "memory" + if not src.is_dir(): + return + dst_dir = REPO_ROOT / ".workbuddy-ai" / "memory" + dst_dir.mkdir(parents=True, exist_ok=True) + for f in sorted(src.glob("*.md")): + text = f.read_text(encoding="utf-8", errors="replace").strip() + if not text: + continue + dst = dst_dir / f.name + marker = "## [task-worktree 归并自任务 {} / {}]".format(st["id"], st["slug"]) + with dst.open("a", encoding="utf-8", newline="\n") as fh: + fh.write("\n" + marker + "\n\n" + text + "\n") + print("[ok] 记忆归并 {} -> {}".format(f.name, dst)) + + +def teardown_worktree(st: dict, force: bool = False, base: str = None) -> None: + wt = Path(st["path"]) + base = base or st.get("base") + # 1) 先摘联接——绝不能让 git 递归进联接去删目标内容(.venv / SDL2 会被一起删掉) + for rel in st.get("junctions", []): + if remove_junction(wt / rel): + print("[ok] 已摘除联接 {}".format(rel)) + # 2) 归并任务侧记忆 + if wt.is_dir(): + harvest_memory(st) + # 3) 移除工作树 + r = git("worktree", "remove", *(["--force"] if force else []), str(wt), check=False) + if r.returncode != 0: + raise Fail("移除工作树失败:{}\n(提示:先处理未跟踪文件,或加 --force)".format( + (r.stdout + r.stderr).strip())) + # 4) 删分支。⚠ 不用 `git branch -d`——它按**环境 HEAD** 判断是否已合并, + # 集成点被 DTU_INTEGRATION_WORKTREE 改走时会误判。改为显式核对 base。 + if force or git("merge-base", "--is-ancestor", st["branch"], base, + check=False).returncode == 0: + r = git("branch", "-D", st["branch"], check=False) + if r.returncode != 0: + print("[warn] 删除分支 {} 失败:{}".format( + st["branch"], (r.stdout + r.stderr).strip())) + else: + print("[ok] 已删除分支 {}".format(st["branch"])) + else: + print("[warn] 分支 {} 未并入 {},保留不删(确认可弃再 `git branch -D {}`)".format( + st["branch"], base, st["branch"])) + state_file(st["id"]).unlink(missing_ok=True) + + +# --------------------------------------------------------------------- 子命令 + + +def cmd_new(a) -> int: + if not ID_RE.match(a.task_id): + raise EnvError("任务 ID 非法:{}(字母数字开头,可用 . _ -)".format(a.task_id)) + if not SLUG_RE.match(a.slug): + raise EnvError("slug 非法:{}(小写字母数字与连字符)".format(a.slug)) + branch = "{}{}-{}".format(BRANCH_PREFIX, a.task_id, a.slug) + wt = WT_ROOT / "{}-{}".format(a.task_id, a.slug) + + if state_file(a.task_id).is_file(): + raise EnvError("任务 {} 已存在({})".format(a.task_id, state_file(a.task_id))) + if os.path.lexists(str(wt)): + raise EnvError("目录已存在:{}".format(wt)) + if git("rev-parse", "--verify", "--quiet", "refs/heads/" + branch, check=False).returncode == 0: + raise EnvError("分支已存在:{}".format(branch)) + if git("rev-parse", "--verify", "--quiet", a.base, check=False).returncode != 0: + raise EnvError("基线分支不存在:{}".format(a.base)) + + active = [s for s in all_states() if Path(s["path"]).is_dir()] + if len(active) >= MAX_CONCURRENT and not a.force: + raise EnvError("已有 {} 个活跃任务(上限 {});先 done/abort,或加 --force".format( + len(active), MAX_CONCURRENT)) + + WT_ROOT.mkdir(parents=True, exist_ok=True) + print("[1/3] 建工作树(分支 {},基线 {}{})…".format( + branch, a.base, ",跳过 LFS smudge" if a.lean else "")) + git("worktree", "add", "-b", branch, str(wt), a.base, + env={"GIT_LFS_SKIP_SMUDGE": "1"} if a.lean else None) + + print("[2/3] 建立共享联接…") + junctions = [] if a.no_bootstrap else bootstrap(wt) + + st = { + "id": a.task_id, "slug": a.slug, "branch": branch, "path": str(wt), + "base": a.base, "created_at": time.strftime("%Y-%m-%d %H:%M:%S"), + "junctions": junctions, "needs_bench": bool(a.needs_bench), + "bench_pass": False, "note": a.note or "", "lean": bool(a.lean), + } + save_state(st) + print("[3/3] 台账已写入 {}".format(state_file(a.task_id))) + print() + print("下一步:把新会话的工作目录切到") + print(" " + str(wt)) + print("在该目录里干活、跑 @build 与 ci_local.py(并行安全);") + print("要动台架先 `lock acquire bench --task {}`,完事 `lock release bench --task {} --bench-pass`。".format( + a.task_id, a.task_id)) + print("回收用 `done {}`(**必须在主工作树执行**)。".format(a.task_id)) + return EXIT_OK + + +def cmd_list(a) -> int: + states = all_states() + if not states: + print("(没有任务空间。用 `new ` 建一个)") + return EXIT_OK + print("{:<12} {:<32} {:>5} {:>5} {:>4} {}".format( + "任务", "分支", "落后", "领先", "脏", "台架")) + for st in states: + wt = Path(st["path"]) + if not wt.is_dir(): + print("{:<12} {:<32} {:>5} {:>5} {:>4} {}".format( + st["id"], st["branch"], "-", "-", "-", "[工作树缺失]")) + continue + ab = git_out("rev-list", "--left-right", "--count", + "{}...{}".format(st["base"], st["branch"]), cwd=wt, check=False).split() + behind, ahead = (ab[0], ab[1]) if len(ab) == 2 else ("?", "?") + dirty = len(git_out("status", "--porcelain", cwd=wt, check=False).splitlines()) + bench = "PASS" if st.get("bench_pass") else ("需测" if st.get("needs_bench") else "-") + print("{:<12} {:<32} {:>5} {:>5} {:>4} {}".format( + st["id"], st["branch"], behind, ahead, dirty, bench)) + print() + for n in LOCK_NAMES: + info = lock_read(n) + if not info: + print("锁 {:<6} 空闲".format(n)) + else: + print("锁 {:<6} {} 持有(已 {:.1f} 小时,于 {})".format( + n, info.get("task"), lock_age_hours(info), info.get("at"))) + return EXIT_OK + + +def cmd_status(a) -> int: + st = load_state(a.task_id) + wt = Path(st["path"]) + print("任务 {} ({})".format(st["id"], st["slug"])) + print("分支 {} ← 基线 {}".format(st["branch"], st["base"])) + print("工作树 {}{}".format(wt, "" if wt.is_dir() else " [缺失]")) + print("建立于 {}".format(st.get("created_at"))) + if st.get("note"): + print("备注 {}".format(st["note"])) + if wt.is_dir(): + dirty = git_out("status", "--porcelain", cwd=wt, check=False).splitlines() + print("未提交 {} 项".format(len(dirty))) + for line in dirty[:10]: + print(" " + line) + ab = git_out("rev-list", "--left-right", "--count", + "{}...{}".format(st["base"], st["branch"]), cwd=wt, check=False).split() + if len(ab) == 2: + print("相对基线 落后 {} / 领先 {} 个提交".format(ab[0], ab[1])) + print("台架 {}{}".format( + "需要," if st.get("needs_bench") else "不涉及,", + "已记录 PASS" if st.get("bench_pass") else "尚无 PASS 记录")) + if st.get("junctions"): + print("联接 {}".format(", ".join(st["junctions"]))) + return EXIT_OK + + +def cmd_sync(a) -> int: + st = load_state(a.task_id) + wt = Path(st["path"]) + base = a.base or st["base"] + if not wt.is_dir(): + raise EnvError("工作树不存在:{}".format(wt)) + if git_out("status", "--porcelain", cwd=wt, check=False): + raise Fail("任务工作树有未提交改动,先提交或 stash 再 sync") + r = git("rebase", base, cwd=wt, check=False) + if r.returncode != 0: + print((r.stdout + r.stderr).strip()) + raise Fail("rebase 有冲突:在 {} 里解决后 `git rebase --continue`,放弃用 `git rebase --abort`".format(wt)) + print("[ok] 任务 {} 已 rebase 到 {}".format(st["id"], base)) + return EXIT_OK + + +def cmd_lock(a) -> int: + if a.action == "status": + for n in LOCK_NAMES: + info = lock_read(n) + if not info: + print("{:<6} 空闲".format(n)) + else: + age = lock_age_hours(info) + mark = " [陈旧,可被自动接管]" if age > LOCK_TTL_HOURS else "" + print("{:<6} {} 持有(pid={},于 {},已 {:.1f} 小时,备注 {}){}".format( + n, info.get("task"), info.get("pid"), info.get("at"), age, + info.get("note") or "-", mark)) + return EXIT_OK + + if a.name not in LOCK_NAMES: + raise EnvError("未知资源 {}(可用:{})".format(a.name, " / ".join(LOCK_NAMES))) + + if a.action == "acquire": + if not a.task: + raise EnvError("acquire 需要 --task ") + got, info = lock_acquire(a.name, a.task, wait=a.wait, force=a.force, note=a.note or "") + if not got: + print("获取失败:{} 被 {} 占用(pid={},于 {})".format( + a.name, info.get("task"), info.get("pid"), info.get("at"))) + return EXIT_FAIL + print("已获取 {} 锁(task={} pid={})".format(a.name, a.task, os.getpid())) + return EXIT_OK + + if a.bench_pass: + if not a.task: + raise EnvError("--bench-pass 需要 --task ") + st = load_state(a.task) + st["bench_pass"] = True + save_state(st) + print("[ok] 已为任务 {} 记录台架 PASS".format(a.task)) + ok, msg = lock_release(a.name, a.task, force=a.force) + print(msg) + return EXIT_OK if ok else EXIT_FAIL + + +def cmd_done(a) -> int: + st = load_state(a.task_id) + wt = Path(st["path"]) + branch = st["branch"] + base = a.base or st["base"] + main = main_worktree() + + print("=== 前置检查 ===") + if not wt.is_dir(): + raise EnvError("工作树不存在:{}".format(wt)) + if git_out("status", "--porcelain", cwd=wt, check=False): + raise Fail("任务工作树有未提交改动,先提交") + if git_out("status", "--porcelain", cwd=main, check=False): + raise Fail("主工作树不干净({})——合并会把未提交改动卷进来".format(main)) + cur = git_out("rev-parse", "--abbrev-ref", "HEAD", cwd=main) + if cur != base: + raise Fail("主工作树当前在 {},不是集成分支 {};先切过去再 done".format(cur, base)) + for n in LOCK_NAMES: + info = lock_read(n) + if info: + raise Fail("独占资源 {} 仍被 {} 占用(已 {:.1f} 小时),先 release".format( + n, info.get("task"), lock_age_hours(info))) + + ahead = int(git_out("rev-list", "--count", "{}..{}".format(base, branch), cwd=wt) or "0") + already = git("merge-base", "--is-ancestor", branch, base, cwd=main, check=False).returncode == 0 + if ahead == 0 and not already: + raise Fail("任务分支相对 {} 没有新提交,无需合并".format(base)) + if already and ahead == 0: + print("[i] {} 已合入 {},跳过合并,直接清场".format(branch, base)) + print("=== 清场 ===") + teardown_worktree(st, force=a.force, base=base) + print("\n完成(分支此前已合入)。") + return EXIT_OK + + if st.get("needs_bench") and not st.get("bench_pass"): + raise Fail("任务标记 --needs-bench 但无台架 PASS 记录;跑完 @test 后执行 " + "`lock release bench --task {} --bench-pass`".format(st["id"])) + print("[ok] 前置检查通过(领先 {} 共 {} 个提交)".format(base, ahead)) + + print("=== 合并预演(git merge-tree,不改动任何东西)===") + r = git("merge-tree", "--write-tree", base, branch, cwd=main, check=False) + if r.returncode != 0: + print((r.stdout + r.stderr).strip()) + raise Fail("预演发现冲突:到任务工作树里 `git merge {}` 解决并提交后重跑 done".format(base)) + tree = (r.stdout.splitlines() or ["?"])[0].strip() + print("[ok] 无冲突,预演结果树 {}".format(tree)) + if a.dry_run: + print() + print("--dry-run:到此为止,未做任何改动。") + return EXIT_OK + + print("=== 合入主干({},--no-ff)===".format(main)) + base_before = git_out("rev-parse", "HEAD", cwd=main) + msgfile = write_merge_message(st, base, wt) + r = git("merge", "--no-ff", "--no-edit", branch, "-F", str(msgfile), cwd=main, check=False) + msgfile.unlink(missing_ok=True) + if r.returncode != 0: + raise Fail("合并失败:\n{}".format((r.stdout + r.stderr).strip())) + print((r.stdout or "").strip()) + base_after = git_out("rev-parse", "HEAD", cwd=main) + + print("=== 合并残留审计 ===") + audit_residue(base_before, branch, cwd=main) + + print("=== 重生成受管产物 ===") + regen(main) + + if a.skip_gates: + print("=== 门禁(--skip-gates 已跳过)===") + gates_ok = True + else: + print("=== 门禁 ===") + gates_ok = run_gates(main, skip_build=a.skip_build) + + if not gates_ok: + print() + print("!! 门禁未全绿:工作树与分支**保留**,未清场。") + print(" 主干已含合并提交 {}".format(base_after[:12])) + print(" 可选:a) 在主工作树修正后追加提交(推荐)") + print(" b) 回退本次合并:git -C \"{}\" reset --hard {}".format(main, base_before[:12])) + return EXIT_FAIL + + print("=== 清场 ===") + teardown_worktree(st, force=a.force, base=base) + print() + print("完成。主干 {} 现为 {}。".format(base, base_after[:12])) + print("push 需人工许可,本次未推送。") + return EXIT_OK + + +def cmd_abort(a) -> int: + st = load_state(a.task_id) + wt = Path(st["path"]) + if wt.is_dir(): + dirty = git_out("status", "--porcelain", cwd=wt, check=False).splitlines() + ahead = int(git_out("rev-list", "--count", + "{}..{}".format(st["base"], st["branch"]), cwd=wt, + check=False) or "0") + if (dirty or ahead) and not a.force: + print("任务 {} 有 {} 项未提交改动、{} 个未合并提交,abort 会**永久丢弃**:".format( + st["id"], len(dirty), ahead)) + for line in dirty[:20]: + print(" " + line) + raise Fail("确认丢弃请加 --force") + teardown_worktree(st, force=True) + print("\n已放弃任务 {}(工作树与分支均已删除)".format(st["id"])) + return EXIT_OK + + +# --------------------------------------------------------------------- 入口 + + +def build_parser(): + ap = argparse.ArgumentParser( + prog="task_worktree.py", + description="多任务并行开发的空间管理(git worktree 生命周期)") + sub = ap.add_subparsers(dest="cmd") + + p = sub.add_parser("new", help="新建任务空间") + p.add_argument("task_id") + p.add_argument("slug") + p.add_argument("--base", default=BASE_BRANCH, help="基线分支(默认 %(default)s)") + p.add_argument("--lean", action="store_true", + help="跳过 Git LFS smudge(省约 38MB,参考图变成指针文件)") + p.add_argument("--no-bootstrap", action="store_true", help="不建共享联接") + p.add_argument("--needs-bench", action="store_true", + help="需台架验证;无 PASS 记录则 done 拒绝合并") + p.add_argument("--note", default="") + p.add_argument("--force", action="store_true", help="忽略并发上限") + p.set_defaults(func=cmd_new) + + p = sub.add_parser("list", help="列出所有任务空间") + p.set_defaults(func=cmd_list) + + p = sub.add_parser("status", help="单个任务详情") + p.add_argument("task_id") + p.set_defaults(func=cmd_status) + + p = sub.add_parser("sync", help="rebase 追平主干") + p.add_argument("task_id") + p.add_argument("--base", default=None) + p.set_defaults(func=cmd_sync) + + p = sub.add_parser("lock", help="独占资源锁(build / bench)") + p.add_argument("action", choices=["acquire", "release", "status"]) + p.add_argument("name", nargs="?", choices=list(LOCK_NAMES)) + p.add_argument("--task", default=None) + p.add_argument("--wait", type=int, default=0, help="等待秒数(0=不等待)") + p.add_argument("--force", action="store_true") + p.add_argument("--note", default="") + p.add_argument("--bench-pass", action="store_true", + help="release bench 时顺带为该任务记录台架 PASS") + p.set_defaults(func=cmd_lock) + + p = sub.add_parser("done", help="回收:预演 → 合入主干 → 审计 → 重生成 → 门禁 → 清场") + p.add_argument("task_id") + p.add_argument("--base", default=None) + p.add_argument("--dry-run", action="store_true", help="只预演,不做任何改动") + p.add_argument("--skip-build", action="store_true", help="跳过 @build") + p.add_argument("--skip-gates", action="store_true", help="跳过全部门禁") + p.add_argument("--force", action="store_true", help="清场时强制移除工作树") + p.set_defaults(func=cmd_done) + + p = sub.add_parser("abort", help="放弃任务(不合并)") + p.add_argument("task_id") + p.add_argument("--force", action="store_true") + p.set_defaults(func=cmd_abort) + return ap + + +def main(argv=None) -> int: + ap = build_parser() + a = ap.parse_args(argv) + if not getattr(a, "func", None): + ap.print_help() + return EXIT_USAGE + if not (REPO_ROOT / ".git").exists(): + print("!! 无法定位仓库根(推导为 {})".format(REPO_ROOT), file=sys.stderr) + return EXIT_ENV + try: + return a.func(a) + except EnvError as e: + print("前置/环境错误:{}".format(e), file=sys.stderr) + return EXIT_ENV + except Fail as e: + print("失败:{}".format(e), file=sys.stderr) + return EXIT_FAIL + except KeyboardInterrupt: + print("已中断", file=sys.stderr) + return EXIT_FAIL + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.dsh/skills/ui-verify/SKILL.md b/.dsh/skills/ui-verify/SKILL.md index 370e4d2f..10c1b735 100644 --- a/.dsh/skills/ui-verify/SKILL.md +++ b/.dsh/skills/ui-verify/SKILL.md @@ -1,13 +1,18 @@ --- name: ui-verify description: UI 改动后的回归验证:5 步门禁(模拟器构建、菜单像素回归、内容页黄金参考、Keil 构建、代码规范)。 -whenToUse: Drivers/BSP/UI、字体 ui_font_cn_12.c / ui_cn_chars.txt、菜单/内容页被修改之后,或用户要求"验证 UI / 跑 UI 回归 / UI 改动检查"时。 +whenToUse: Drivers/BSP/UI、字体 ui_font_cn_12.c / ui_cn_chars.txt、菜单/内容页被修改的前后(写码前先读「两条实证坑」节),或用户要求"验证 UI / 跑 UI 回归 / UI 改动检查"时。 --- # UI 回归验证 UI 相关改动完成后必须跑完 5 个门禁再提交。完整说明见 `docs/UI设计/UI_自动化测试流程.md`。 +## 两条实证坑(写码前先读,模拟器照不出来) + +- **I1 绘制缓冲含 8B palette 头**:布局 `[palette 8B][pixel data]`,但 LVGL 容量校验只看 `stride×h` → 只给 `stride×h` 会把末 8 字节写到缓冲外。`ui_port.c` 按 `stride×h + palette` 供给并在 flush 时 `+palette` → `docs/UI设计/UI_设计架构.md` §1.4 第 8 条 +- **MCU 端必须显式应用 mono 主题**:`lv_theme_mono_init(disp, true, ...)` + `lv_display_set_theme()` + 对活动屏幕再 `lv_theme_apply()`,否则活动屏仍是默认浅灰而内容恒用白色 → **白底白字整屏纯白**(远端"纯白屏"根因)→ `docs/UI设计/UI_设计架构.md` §1.4 第 9 条 + ## 步骤(仓库根目录执行,python 一律用 .venv) ```powershell diff --git a/.gitignore b/.gitignore index 8896ca94..4d0dd662 100644 --- a/.gitignore +++ b/.gitignore @@ -133,6 +133,12 @@ docs/images/*.png # Serial assistant runtime state (daemon log/meta, local runtime artifacts) .dsh/skills/serial-assistant/scripts/state/ +# 记忆工具(Mnemon)本地状态:项目文档索引 documents/ + 运行时记忆 +# runtime/MEMORY.md、USER.md、memories.json。**只留本机、不进版本库** +# (2026-10-11 定):记忆是按机器/按人累积的私有状态,入库会让每次记忆 +# 写入都变成提交抖动,还可能带出本机绝对路径与个人偏好。 +.mnemon/ + # CI 运行时产物(.gitea/workflows/ci.yml 的 S3 步骤在 runner 工作区落盘的日志; # 零依赖存档方案——落在工作区目录而非上传 artifact,故必须忽略以免误提交) ci-artifacts/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ace93544..8aceebb4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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/ ) @@ -147,17 +154,18 @@ repos: # 5) 文档一致性门禁(D-7) # 存在性:md 互链 / 反引号路径是否指向真实文件(防迁移/删除后残留死链) + # 章节:`§N.M` 必须能在目标文档的编号标题里找到(--sections-strict:自引用失效也拦) - id: doc-links name: 文档链接存在性 (D-3/D-7) - entry: python scripts/check_doc_links.py + entry: python scripts/check_doc_links.py --sections-strict language: system files: \.md$ # 外部资料(芯片手册转录)链接形态不受控;第三方目录整体排除 + # (`simulator/` 已不再排除:其下 3 个 README 是本项目文档,2026-10-11) exclude: | (?x)^( \.opencode/node_modules/| Lib/lvgl/| - simulator/| MDK-ARM/RTE/| docs/芯片手册/ ) @@ -177,6 +185,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)、加固宏被删)。 @@ -232,16 +261,178 @@ repos: files: ^(App/cfg/cfg_file\.(c|h)|App/cfg/cfg_file_store\.h|App/log/sys_log\.h|test/host/cfg_file/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false + # 7d) 相量层(L2)主机侧单元测试(2026-10-10 加) + # 背景:App/algo/ 为**纯算法**层(无寄存器、无 RTOS、无外设依赖),是本项目 + # 首个可完全离线验证的算法层。板端**无信号源**,无法注入已知幅值/相位/频率的 + # 标准波形,故"读数 = 设定值"这一核心判据在板上不可验;本钩子用 MSVC 离线编译 + # 真实源文件(#include 纳入,无需桩)+ 合成任意波形,把系数表、幅值/相位、 + # 直流与谐波抑制、频偏敏感度、滑动窗、双区交换一次性钉死。 + # files 含被测的两个源与其头,以及 harness 本身。 + - id: phasor-unit-test + name: 相量层单测(主机侧) + entry: python test/host/phasor/run_unit_test.py + language: system + files: ^(App/algo/(phasor|sample_ring)\.(c|h)|test/host/phasor/.*|test/host/common/unit_test_harness\.py)$ + pass_filenames: false + + # 7e) ADC 输入层(L1)主机侧单元测试(2026-10-10 加) + # 背景:adc_input.c 的核心是"通道号 <-> Rank 顺序 <-> 语义"三者映射的正确性; + # 板端无信号源,无法注入已知电压验证"读到的是哪只脚"(映射说明 §8)。本钩子用 + # MSVC 离线编译真实源文件(#include 纳入)+ HAL/TPAFE 可注入桩:给每个 Rank + # 注入互不相同的码值以**逐通道钉死映射**,并一次钉死 HAL 启动失败 / 转换超时 / + # 参数错等真机难复现的分支。 + # files 含被测源与其头、两个桩头所在目录,以及 harness 本身。 + - id: adc-input-unit-test + name: ADC 输入层单测(主机侧) + entry: python test/host/adc_input/run_unit_test.py + language: system + files: ^(App/algo/adc_input\.(c|h)|test/host/adc_input/.*|test/host/common/unit_test_harness\.py)$ + pass_filenames: false + + # 7f) 卷锁(vol_lock)主机侧单元测试(2026-10-10 加) + # 背景:vol_lock 的**核心不变量是 fail-open**——互斥量创建失败时 + # `vol_lock_wait()` 必须**返回 1 且绝不阻塞**,因为它的调用者 defaultTask 是 + # 看门狗喂狗点,一旦在此阻塞即整机复位。板端该路径**只在内存耗尽时才可能走到**, + # 无法人为复现;本钩子用 MSVC 离线编译真实源文件(#include 纳入)+ CMSIS 互斥桩, + # 把"创建失败 -> 直接放行"、"创建成功 -> 正常加解锁"、"未持锁释放"、 + # 超时参数透传等分支一次性钉死。 + # files 含被测源与其头,以及 harness 本身。 + - id: vol-lock-unit-test + name: 卷锁单测(主机侧) + entry: python test/host/vol_lock/run_unit_test.py + language: system + files: ^(App/fs/vol_lock\.(c|h)|test/host/vol_lock/.*|test/host/common/unit_test_harness\.py)$ + pass_filenames: false + + # 7g) 时间同步(time_sync)主机侧单元测试(2026-10-10 加) + # 背景:time_sync.c 是 TCP 时间同步协议状态机,也是 N-17 号看门狗风险的**全部 + # 逻辑贡献者**——"listen 重建 -> 800ms~1s 心跳间隔"由本模块的分片延时/退避/ + # 结算等待组成,板端只能看到"是否复位"这一结果,看不到各段延时是否如设计。本钩子 + # 用 MSVC 离线编译真实源文件(#include 纳入)+ 可脚本化的网络/事件桩(含 longjmp + # 跳出 for(;;) 任务循环),把分片喂狗节奏、监听退避、报文重组(半包/粘连/EOF/真错/ + # 预算耗尽/tick 回绕)、重听门控逐条钉死。 + # files 含被测源与其头、被测的真实看门狗头,以及 harness 本身。 + - id: time-sync-unit-test + name: 时间同步单测(主机侧) + entry: python test/host/time_sync/run_unit_test.py + language: system + files: ^(App/sync/time_sync\.(c|h)|App/util/task_watchdog\.h|test/host/time_sync/.*|test/host/common/unit_test_harness\.py)$ + pass_filenames: false + + # 7h) ADC 采样有效性校验(L1-6)主机侧单元测试(2026-10-11 加) + # 背景:adc_health.c 是"静默/丢帧/序号倒退/错误计数增量"的判定层——这些 + # 故障在健康台架上**不可复现**(复现 = 拔芯片/改驱动),板端闭环只能证明 + # "健康时不误报",证不了"故障时必报";100ms 门限、tick 回绕、事件边沿 + # 语义等边界只有合成计数序列能精确构造。本钩子用 MSVC 离线编译真实源文件 + # (#include 纳入,纯逻辑零桩)把 11 组判据一次性钉死。 + # files 含被测源与其头、adc_task.c(告警编排的消费方),以及 harness 本身。 + - id: adc-health-unit-test + name: ADC 有效性校验单测(主机侧) + entry: python test/host/adc_health/run_unit_test.py + language: system + 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 + + # 7k) SHA-256 主机侧单元测试(2026-10-11 加,N-19 FTP 鉴权) + # sha256 是 FTP pwd_hash 校验的地基——哈希算错是"正确口令被拒 / + # 错误口令放行"的静默语义错误,板端只见 530/230 看不出根因; + # FIPS 180-4 标准向量(空串/abc/56B 补位跨界/1MB)+ hex_eq 非法输入 + # 在主机侧毫秒级钉死。零平台依赖,无桩。 + - id: sha256-unit-test + name: SHA-256 单测(主机侧) + entry: python test/host/sha256/run_unit_test.py + language: system + files: ^(App/util/sha256\.(c|h)|test/host/sha256/.*|test/host/common/unit_test_harness\.py)$ + pass_filenames: false + + # 7l) FTP 鉴权状态机主机侧单元测试(2026-10-11 加,N-19) + # 锁定 60s/解锁/成功清零/tick 32 位回绕是"防口令爆破"契约核心, + # 板端每验一条要真等 60s、回绕要跑满 49.7 天;lftpd_auth 纯函数 + # 设计(tick 参数注入)就是为了让这些分支主机侧毫秒级穷举。 + - id: ftp-auth-unit-test + name: FTP 鉴权状态机单测(主机侧) + entry: python test/host/ftp_auth/run_unit_test.py + language: system + files: ^(Drivers/BSP/NET/lftpd/lftpd_auth\.(c|h)|App/util/sha256\.(c|h)|test/host/ftp_auth/.*|test/host/common/unit_test_harness\.py)$ + pass_filenames: false + + # 7m) IEC 60870-5-104 被控站协议栈主机侧单元测试(2026-10-12 加,B4) + # APCI 重组/STARTDT/总召分片/COV/k-w-t1~t3/遥控定值两段式/C_CS/错误 + # 应答——link 层 tick 注入 + pts 数据源 vtable 抽象就是为 L0.5 全离线 + # 穷举设计(A-4 约束 1);板端复现这些分支要真主站+真等待。 + # 触发面含数据源依赖(prot_set/prot_elm/phasor/sample_ring)——测试 + # 直接 #include 这些生产源,签名变了要立刻在这里暴露。 + - id: iec104-unit-test + name: IEC104 协议栈单测(主机侧) + entry: python test/host/iec104/run_unit_test.py + language: system + files: ^(App/protocol/iec104/iec104_(asdu|pts|link)\.(c|h)|App/algo/(prot_set|prot_elm|phasor|sample_ring|adc_input)\.(c|h)|test/host/iec104/.*|test/host/common/unit_test_harness\.py)$ + pass_filenames: false + # 8) 测试套件开关门禁(2026-10-01 加) # 背景:test_config.h 的 TEST_SUITE_* 决定固件里编入哪个测试套件, # 全部注释 = 产品构建。但此前**无任何门禁检查**——实测已提交的 HEAD 里 # TEST_SUITE_CH395F 就是开启状态(.map 含 34 处 test 代码),即仓库里 # 躺着的是测试固件。本钩子抓这个静默漏网:默认要求产品构建,跑套件回归 # 确需提交测试构建时用 `--allow-test-build` / ALLOW_TEST_BUILD=1 显式放行。 - # 详见 docs/待处理内容.md T-1 + # 详见 docs/任务规划/登记簿.md T-1 - id: test-suite-switch name: 测试套件开关=产品构建 entry: python scripts/check_test_suite.py language: system files: ^test/board/test_config\.h$ pass_filenames: false + + # 9) 双端注册反向校验(2026-10-12 加,登记簿 N-23 / 审查报告 N-06 残余) + # 背景:新 .c 必须手动注册两处(`MDK-ARM/*.uvprojx` + `simulator/CMakeLists.txt`) + # 才会被编译;历史上 `App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c` + # **两处都没注册** = 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效。 + # 本钩子反向核对两份注册表:①存在但未进 uvprojx ②存在但未进 CMakeLists + # (MCU-only 豁免见脚本 SIM_EXEMPT,显式清单+每行理由,不整目录一刀切) + # ③注册了但文件不存在(幽灵注册)。 + # 存量缺口(rs485_task/lftpd_log)已进脚本 KNOWN_GAPS 白名单 → WARN 不拦, + # 只对新缺口 FAIL,故钩子可立即上线不拦现有提交(`--strict` 供审计关白名单)。 + # ⚠ pre-commit 4.x 已移除 `always: true` → files 必须覆盖所有触发面: + # 任意 .c + uvprojx + CMakeLists.txt + 脚本自身(脚本自身改动也值得复跑)。 + - id: registration-check + name: 双端注册反向校验 (uvprojx + CMakeLists) + entry: python scripts/check_registration.py + language: system + files: (\.c$|\.uvprojx$|CMakeLists\.txt$|^scripts/check_registration\.py$) + pass_filenames: false + # 10) RAM 预算 92% 阈值门禁(2026-10-12 加,登记簿 N-27 前半 / 审查报告 §5.5 三件套) + # 背景:报告 §5.5 第 1 条要求"超阈值即失败 + Top-10 附表"——此前 mem-numbers 只防 + # "文档数字漂移",**没有任何机械检查拦"占用超阈值"**;台账三件套(ram_baseline.py + + # ram_baseline.tsv + docs/内存管理/RAM预算台账.md)落地后,本钩子把 92% 红线机械化。 + # 逻辑:RW_IRAM1/RW_CCM 任一 >92% → FAIL + Top-10 RW+ZI 对象表;台账受管区漂移 → FAIL + # (提示 --write);无 .map → 自适应跳过(同 mem-numbers 在 CI 的行为,盲区登记 A-5 残余)。 + # ⚠ pre-commit 4.x 无 `always: true` → files 覆盖所有能改变 RAM 的触发面: + # 全部自研代码目录 + lv_conf.h + 测试板配置 + 脚本/tsv/台账自身 + 本配置文件。 + - id: mem-budget + name: RAM 预算 92% 阈值门禁 (N-27) + entry: python scripts/ram_baseline.py --gate + language: system + files: ^(App/|Drivers/BSP/|Src/|Inc/|Lib/lv_conf\.h|test/board/|scripts/ram_baseline\.|docs/内存管理/|\.pre-commit-config\.yaml$) + pass_filenames: false diff --git a/.serialtest/config.json b/.serialtest/config.json index 4f89d139..59a7b3ef 100644 --- a/.serialtest/config.json +++ b/.serialtest/config.json @@ -7,7 +7,7 @@ "fw_marker": "fw build: ", "readback_cmd": ".venv\\Scripts\\python.exe .dsh\\skills\\stlink-flash\\scripts\\stlink_flash.py cmp {hex}", - "required": "App init OK;Network Init;net init: OK", + "required": "App init OK;Network Init;net init: OK;protect task start;i104: listening on 2404;[STACK] protect;[STACK] iec104;[STACK] heap", "forbidden": "FAIL;ERROR;HardFault;[ERR]", "boot_marker": "reset cause:", "max_boot_count": 1, diff --git a/AGENTS.md b/AGENTS.md index 46a93876..c46a9c43 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,62 +1,53 @@ # STM32F4-Base -STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v5 (ARMCC, C99) 工具链**;FreeRTOS V10.3.1 / CMSIS-RTOS2;192KB SRAM / 1MB Flash。 +**工业级嵌入式 DTU 平台固件**:STM32F407ZGTx @168MHz,集成以太网(CH395F)、SPI NAND 存储(GD5F)、电气量采集(TPAFE5160 + 片内 ADC)、RS-485、FTP 文件服务,自带远程显示(UI 推流)、墙钟对时、看门狗自恢复、运行/自检记录——既是可独立部署的现场设备,也是**可复用的平台底座**(保护测控等后续产品在此底座上补齐)。产品基线 → `docs/产品需求/产品需求设计文档.md` §2.1。 + +**仅 Keil MDK-ARM v5 (ARMCC, C99) 工具链**;FreeRTOS V10.3.1 / CMSIS-RTOS2;192KB SRAM / 1MB Flash。 **任何代码修改后必须先 `@build` 验证(0 错误 0 警告)再提交;硬件闭环用 `@test`。** -## 铁律:源码编辑与编码 +> 本文件 = agent 入口:宏观概况 + 每次都在场的铁律在这里,细节在 `docs/` 与 skills(文末索引)。同一事实只在一处详述(`docs/文档规范.md` 同此要求)。 -- 改代码**只用 Edit 工具**(精确子串替换);批量替换用 Edit 的 replaceAll。**绝不用 `Get-Content`/`Set-Content`/`cmd` 重写含中文的文件**——整文件重写会按系统默认编码重解码 → `U+FFFD` 乱码且不可逆 -- **字符串字面量必须纯 ASCII**(2026-09-15 实测):本机 ARMCC 对**无 BOM** 源文件按 CP936 解读,字符串内含**奇数个**汉字时末尾高字节吞掉结束引号 → 编译错(偶数个汉字侥幸通过);中文只写注释(注释不参与解析)。带 BOM 的文件(`Drivers/BSP/UI/*`、`App/task/ui_task.c`、`App/cfg/cfg_file.c`、`Lib/lv_conf.h`)经 BOM 按 UTF-8 解码,不受此条限制 -- 确需脚本级重写:工程源码为 UTF-8(多数文件无 BOM,UI 系带 BOM),按原样同编码回写、保持 BOM 状态;拿不准就别碰 -- 发现乱码:先统计 `EF BF BD` 字节数确认真损坏(勿信终端/Read 渲染);**不要急著 `git checkout`**(会丢弃全部未提交工作)——先用 Edit 只修坏行 -- **Edit 工具会剥掉文件原有的 UTF-8 BOM**(2026-09-18 实测):带 BOM 文件(如 `App/cfg/cfg_file.c`)被 Edit 后 BOM 静默消失 → ARMCC 改按 CP936 解码 → 中文字符串字面量报 `#870-D: invalid multibyte character sequence`(**只有重编译该文件才暴露,历史"0 警告"不代表安全**)。改完带中文字面量的文件须核对 BOM 并按原样补回(只在文件头前缀 3 字节 `EF BB BF`,勿整文件重写) -- **行尾策略已写死进 `.gitattributes`(2026-10-09)**:`* text=auto eol=lf`(仓库 blob 与工作区统一 LF)+ `*.bat`/`*.cmd` 强制 CRLF(cmd.exe 对 LF-only 的 goto/标签不可靠)+ PNG/TTF/DLL/LIB 等显式 `binary` + `*.jpg` 走 LFS 且 `-text`(该条必须排在 `*` 之后才能覆盖)。**不要再依赖 `core.autocrlf`,也不要把工作区文件"改回 CRLF"**——CubeMX 重生成写的就是 LF;此前每次重生成后 40+ 个第三方文件在 `git status` 里显示"已修改"(内容归一化后一致),根因就是行尾由每台机器各自的 autocrlf 决定。第三方/生成文件的"文件末尾空行"已一次性对齐工具输出(`53bd9af`),**勿再用改写型钩子去修剪**(已在 `trailing-whitespace`/`end-of-file-fixer` 豁免) +## 项目概况 -## 构建与验证 +**现状一句话**:平台能力(网络 / 存储 / 时间 / UI / 日志 / 看门狗 / FTP / 配置)**已实现并台架验收**;采集主线已闭环到业务消费者(L1→L2 相量→L3 保护判据 protectTask→104 遥信,B5);**RS-485 采集尚未接线**(B2 按用户决策冻结,见 `docs/任务规划/暂缓-RS485.md`)——当前最大功能缺口,接线节奏受 E-59 门禁约束(见「平台约束」)。 -- `@build` = **`keil-build` skill**:`& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py`(仓库根目录执行;纯标准库,任意 python3 亦可)。退出码 0 = 0 错误 0 警告(PASS)、1 = 构建失败(打印报错行)、2 = UV4 未找到/无效、3 = 用法错误。UV4 解析链 `--uv4` > `.serialtest/config.json` 的 `uv4` > `$DSH_KEIL_UV4` > `$DTU_KEIL_UV4` > PATH(**显式设了但无效 = 立即报错、不静默回退**);日志 `MDK-ARM/build_log.txt`。⚠ **原 `MDK-ARM/build.bat` 已于 2026-10-10 删除**(判定口径同款,但无超时保护、UV4 只认 `DTU_KEIL_UV4`、失败时 dump 整份日志);`test/run_all_suites.py` 已改调本 skill。换机必设 `DTU_KEIL_UV4`,见 `docs/环境与部署/本地工具路径环境变量方案.md` -- `@test` / `.dsh/skills/serial-boot-test/scripts/run_boot_test.py` 一键闭环(2026-09-20 起为纯 Python 标准库实现,替代 PowerShell 版;`& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py`):开串口+清日志 → 烧录(**`stlink-flash` skill**:`stlink_flash.py flash` = `-P -Rst -Run`;`flash_cmd` 默认取 `.serialtest/config.json`,本仓库已指向本 skill)→ **固件身份戳**(从刚烧进去的 hex 里抠 `fw build: + + net_listener.c + 1 + ..\Drivers\BSP\NET\net_listener.c + net_cfg.c 1 ..\Drivers\BSP\NETCFG\net_cfg.c + + ftp_cfg.c + 1 + ..\Drivers\BSP\NETCFG\ftp_cfg.c + nand_ftl.c 1 @@ -1533,6 +1608,11 @@ 1 ..\Drivers\BSP\NET\lftpd\lftpd.c + + lftpd_auth.c + 1 + ..\Drivers\BSP\NET\lftpd\lftpd_auth.c + lftpd_inet.c 1 diff --git a/MDK-ARM/ftp_abort_probe.py b/MDK-ARM/ftp_abort_probe.py index 914b6477..bdfeff75 100644 --- a/MDK-ARM/ftp_abort_probe.py +++ b/MDK-ARM/ftp_abort_probe.py @@ -31,7 +31,9 @@ def rd(sock): def raw_login(ip, port, timeout=10.0): s = socket.create_connection((ip, port), timeout=timeout) rd(s) # 220 banner - s.sendall(b"USER anonymous\r\n"); rd(s) + # N-19: anonymous login is rejected (530) -> use the factory default account + s.sendall(b"USER admin\r\n"); rd(s) + s.sendall(b"PASS admin\r\n"); rd(s) s.sendall(b"TYPE I\r\n"); rd(s) return s @@ -50,7 +52,7 @@ def followup_ok(ip, port, size=4096): data = bytes(random.getrandbits(8) for _ in range(size)) want = hashlib.sha256(data).hexdigest() try: - f = FTP(); f.connect(ip, port, timeout=15.0); f.login("anonymous", "x") + f = FTP(); f.connect(ip, port, timeout=15.0); f.login("admin", "admin") f.storbinary("STOR /ab_ok.bin", io.BytesIO(data)) buf = bytearray(); f.retrbinary("RETR /ab_ok.bin", buf.extend) try: diff --git a/README.md b/README.md index 18837c12..13436d06 100644 --- a/README.md +++ b/README.md @@ -75,11 +75,13 @@ main.c | testTask | 2048 words | BelowNormal | 硬件测试入口 | | uiTask | 4096 words | BelowNormal | LVGL UI 界面 | | timeSyncTask | 512 words | Normal | 墙钟时间同步 | -| adcTask | 512 words | Normal | TPAFE5160 采样(1ms 节拍 + 原子快照) | +| adcTask | 640 words | AboveNormal | TPAFE5160 采样(TIM3 1kHz 事件驱动 + 原子快照 + STM32 ADC DMA) | +| iec104Task | 640 words | Normal | | +| protectTask | 512 words | Normal | | > 上表与 `Src/freertos.c` 的 `osThreadAttr_t` 一致(2026-09-25 校准);改动栈大小前先读 -> `docs/内存管理/RAM使用分析与优化方案.md` 的定栈规则与 `docs/待处理内容.md` 的内存预算项。 +> `docs/内存管理/RAM使用分析与优化方案.md` 的定栈规则与 `docs/任务规划/基线与待确认.md` 的内存预算项。 ### 任务启动顺序管理 @@ -163,7 +165,7 @@ STM32F4-Base/ │ └── time_sync_server.py # 时间同步推送服务 ├── docs/ # 项目文档(全量索引见 docs/README.md) │ ├── README.md # 文档索引:目录结构 + 推荐阅读顺序 + 全量清单 -│ ├── 待处理内容.md # 统一待办登记簿 +│ ├── 任务规划/ # 统一待办登记簿(README 排序权威 + 拆分文件) │ ├── 架构设计/ # 项目架构总览 / 存储架构 / 启动顺序 / 配图流程 / 引用关系图 │ ├── 子系统设计/ # 配置 / 网络 / 墙钟 / 日志 / 运行自检记录 设计说明 │ ├── 网络/ # BSD Socket API 使用指南 + 内部函数调用关系 diff --git a/STM32F407-Demo.ioc b/STM32F407-Demo.ioc index 34a59a7c..54b08b4c 100644 --- a/STM32F407-Demo.ioc +++ b/STM32F407-Demo.ioc @@ -9,7 +9,9 @@ ADC1.Channel-7\#ChannelRegularConversion=ADC_CHANNEL_6 ADC1.Channel-8\#ChannelRegularConversion=ADC_CHANNEL_7 ADC1.Channel-9\#ChannelRegularConversion=ADC_CHANNEL_14 ADC1.ClockPrescaler=ADC_CLOCK_SYNC_PCLK_DIV4 -ADC1.IPParameters=Rank-1\#ChannelRegularConversion,master,Channel-1\#ChannelRegularConversion,SamplingTime-1\#ChannelRegularConversion,NbrOfConversionFlag,ScanConvMode,Rank-2\#ChannelRegularConversion,Channel-2\#ChannelRegularConversion,SamplingTime-2\#ChannelRegularConversion,Rank-3\#ChannelRegularConversion,Channel-3\#ChannelRegularConversion,SamplingTime-3\#ChannelRegularConversion,Rank-4\#ChannelRegularConversion,Channel-4\#ChannelRegularConversion,SamplingTime-4\#ChannelRegularConversion,Rank-5\#ChannelRegularConversion,Channel-5\#ChannelRegularConversion,SamplingTime-5\#ChannelRegularConversion,Rank-6\#ChannelRegularConversion,Channel-6\#ChannelRegularConversion,SamplingTime-6\#ChannelRegularConversion,Rank-7\#ChannelRegularConversion,Channel-7\#ChannelRegularConversion,SamplingTime-7\#ChannelRegularConversion,Rank-8\#ChannelRegularConversion,Channel-8\#ChannelRegularConversion,SamplingTime-8\#ChannelRegularConversion,Rank-9\#ChannelRegularConversion,Channel-9\#ChannelRegularConversion,SamplingTime-9\#ChannelRegularConversion,NbrOfConversion,ClockPrescaler +ADC1.DMAContinuousRequests=ENABLE +ADC1.ExternalTrigConv=ADC_EXTERNALTRIGCONV_T3_TRGO +ADC1.IPParameters=Rank-1\#ChannelRegularConversion,master,Channel-1\#ChannelRegularConversion,SamplingTime-1\#ChannelRegularConversion,NbrOfConversionFlag,ScanConvMode,Rank-2\#ChannelRegularConversion,Channel-2\#ChannelRegularConversion,SamplingTime-2\#ChannelRegularConversion,Rank-3\#ChannelRegularConversion,Channel-3\#ChannelRegularConversion,SamplingTime-3\#ChannelRegularConversion,Rank-4\#ChannelRegularConversion,Channel-4\#ChannelRegularConversion,SamplingTime-4\#ChannelRegularConversion,Rank-5\#ChannelRegularConversion,Channel-5\#ChannelRegularConversion,SamplingTime-5\#ChannelRegularConversion,Rank-6\#ChannelRegularConversion,Channel-6\#ChannelRegularConversion,SamplingTime-6\#ChannelRegularConversion,Rank-7\#ChannelRegularConversion,Channel-7\#ChannelRegularConversion,SamplingTime-7\#ChannelRegularConversion,Rank-8\#ChannelRegularConversion,Channel-8\#ChannelRegularConversion,SamplingTime-8\#ChannelRegularConversion,Rank-9\#ChannelRegularConversion,Channel-9\#ChannelRegularConversion,SamplingTime-9\#ChannelRegularConversion,NbrOfConversion,ClockPrescaler,DMAContinuousRequests,ExternalTrigConv ADC1.NbrOfConversion=9 ADC1.NbrOfConversionFlag=1 ADC1.Rank-1\#ChannelRegularConversion=1 @@ -35,13 +37,24 @@ ADC1.master=1 CAD.formats=[] CAD.pinconfig=Dual CAD.provider=Component Search Engine +Dma.ADC1.6.Direction=DMA_PERIPH_TO_MEMORY +Dma.ADC1.6.FIFOMode=DMA_FIFOMODE_DISABLE +Dma.ADC1.6.Instance=DMA2_Stream4 +Dma.ADC1.6.MemDataAlignment=DMA_MDATAALIGN_HALFWORD +Dma.ADC1.6.MemInc=DMA_MINC_ENABLE +Dma.ADC1.6.Mode=DMA_CIRCULAR +Dma.ADC1.6.PeriphDataAlignment=DMA_PDATAALIGN_HALFWORD +Dma.ADC1.6.PeriphInc=DMA_PINC_DISABLE +Dma.ADC1.6.Priority=DMA_PRIORITY_LOW +Dma.ADC1.6.RequestParameters=Instance,Direction,PeriphInc,MemInc,PeriphDataAlignment,MemDataAlignment,Mode,Priority,FIFOMode Dma.Request0=USART1_TX Dma.Request1=USART1_RX Dma.Request2=SPI2_RX Dma.Request3=SPI2_TX Dma.Request4=SPI1_RX Dma.Request5=SPI1_TX -Dma.RequestsNb=6 +Dma.Request6=ADC1 +Dma.RequestsNb=7 Dma.SPI1_RX.4.Direction=DMA_PERIPH_TO_MEMORY Dma.SPI1_RX.4.FIFOMode=DMA_FIFOMODE_DISABLE Dma.SPI1_RX.4.Instance=DMA2_Stream0 @@ -109,7 +122,7 @@ FREERTOS.Queues01=netMsgQueue,8,net_msg_t,0,Dynamic,NULL,NULL FREERTOS.Tasks01=defaultTask,24,1024,StartDefaultTask,Default,NULL,Dynamic,NULL,NULL;netTask,24,960,StartNetTask,Default,NULL,Dynamic,NULL,NULL;ftpTask,24,1024,StartFtpTask,As weak,NULL,Dynamic,NULL,NULL;testTask,16,2048,start_test_task,As weak,NULL,Dynamic,NULL,NULL;uiTask,16,4096,StartUiTask,Default,NULL,Dynamic,NULL,NULL FREERTOS.configCHECK_FOR_STACK_OVERFLOW=2 FREERTOS.configENABLE_FPU=1 -FREERTOS.configTOTAL_HEAP_SIZE=49152 +FREERTOS.configTOTAL_HEAP_SIZE=57344 FREERTOS.configUSE_MALLOC_FAILED_HOOK=1 File.Version=6 GPIO.groupedBy=Group By Peripherals @@ -121,7 +134,7 @@ Mcu.CPN=STM32F407ZGT6 Mcu.Family=STM32F4 Mcu.IP0=ADC1 Mcu.IP1=DMA -Mcu.IP10=TIM6 +Mcu.IP10=TIM3 Mcu.IP11=UART4 Mcu.IP12=UART5 Mcu.IP13=USART1 @@ -204,7 +217,7 @@ Mcu.Pin65=PE1 Mcu.Pin66=VP_FREERTOS_VS_CMSIS_V2 Mcu.Pin67=VP_IWDG_VS_IWDG Mcu.Pin68=VP_SYS_VS_tim7 -Mcu.Pin69=VP_TIM6_VS_ClockSourceINT +Mcu.Pin69=VP_TIM3_VS_ClockSourceINT Mcu.Pin7=PA4 Mcu.Pin8=PA5 Mcu.Pin9=PA6 @@ -217,9 +230,10 @@ MxDb.Version=DB.6.0.181 NVIC.BusFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false NVIC.DMA1_Stream3_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true NVIC.DMA1_Stream4_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true -NVIC.DMA2_Stream0_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true -NVIC.DMA2_Stream2_IRQn=true\:9\:0\:true\:false\:true\:true\:false\:true\:true +NVIC.DMA2_Stream0_IRQn=true\:7\:0\:true\:false\:true\:false\:false\:true\:true +NVIC.DMA2_Stream2_IRQn=true\:9\:0\:true\:false\:true\:false\:false\:true\:true NVIC.DMA2_Stream3_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true +NVIC.DMA2_Stream4_IRQn=true\:6\:0\:true\:false\:true\:false\:false\:true\:true NVIC.DMA2_Stream7_IRQn=true\:9\:0\:true\:false\:true\:true\:false\:true\:true NVIC.DebugMonitor_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false NVIC.EXTI9_5_IRQn=true\:6\:0\:true\:false\:true\:true\:true\:true\:true @@ -236,7 +250,7 @@ NVIC.SavedPendsvIrqHandlerGenerated=true NVIC.SavedSvcallIrqHandlerGenerated=true NVIC.SavedSystickIrqHandlerGenerated=true NVIC.SysTick_IRQn=true\:15\:0\:false\:false\:false\:true\:false\:true\:false -NVIC.TIM6_DAC_IRQn=true\:6\:0\:true\:false\:true\:true\:true\:true\:true +NVIC.TIM3_IRQn=true\:6\:0\:true\:false\:true\:true\:true\:true\:true NVIC.TIM7_IRQn=true\:15\:0\:true\:false\:true\:false\:false\:true\:true NVIC.TimeBase=TIM7_IRQn NVIC.TimeBaseIP=TIM7 @@ -604,7 +618,7 @@ ProjectManager.ToolChainLocation= ProjectManager.UAScriptAfterPath= ProjectManager.UAScriptBeforePath= ProjectManager.UnderRoot=false -ProjectManager.functionlistsort=1-SystemClock_Config-RCC-false-HAL-false,2-MX_GPIO_Init-GPIO-false-HAL-true,3-MX_DMA_Init-DMA-false-HAL-true,4-MX_USART1_UART_Init-USART1-false-HAL-true,5-MX_SPI2_Init-SPI2-false-HAL-true,6-MX_SPI1_Init-SPI1-false-HAL-true,7-MX_I2C1_Init-I2C1-false-HAL-true,8-MX_UART4_Init-UART4-false-HAL-true,9-MX_UART5_Init-UART5-false-HAL-true,10-MX_USART2_UART_Init-USART2-false-HAL-true,11-MX_USART3_UART_Init-USART3-false-HAL-true,12-MX_IWDG_Init-IWDG-false-HAL-true,13-MX_ADC1_Init-ADC1-false-HAL-true +ProjectManager.functionlistsort=1-SystemClock_Config-RCC-false-HAL-false,2-MX_GPIO_Init-GPIO-false-HAL-true,3-MX_DMA_Init-DMA-false-HAL-true,4-MX_USART1_UART_Init-USART1-false-HAL-true,5-MX_SPI2_Init-SPI2-false-HAL-true,6-MX_SPI1_Init-SPI1-false-HAL-true,7-MX_I2C1_Init-I2C1-false-HAL-true,8-MX_UART4_Init-UART4-false-HAL-true,9-MX_UART5_Init-UART5-false-HAL-true,10-MX_USART2_UART_Init-USART2-false-HAL-true,11-MX_USART3_UART_Init-USART3-false-HAL-true,12-MX_IWDG_Init-IWDG-false-HAL-true,13-MX_ADC1_Init-ADC1-false-HAL-true,14-MX_TIM3_Init-TIM3-false-HAL-true RCC.48MHZClocksFreq_Value=84000000 RCC.AHBFreq_Value=168000000 RCC.APB1CLKDivider=RCC_HCLK_DIV4 @@ -670,10 +684,12 @@ SPI2.Direction=SPI_DIRECTION_2LINES SPI2.IPParameters=VirtualType,Mode,Direction,CalculateBaudRate,BaudRatePrescaler SPI2.Mode=SPI_MODE_MASTER SPI2.VirtualType=VM_MASTER -TIM6.AutoReloadPreload=TIM_AUTORELOAD_PRELOAD_ENABLE -TIM6.IPParameters=Prescaler,Period,AutoReloadPreload -TIM6.Period=999 -TIM6.Prescaler=83 +TIM3.AutoReloadPreload=TIM_AUTORELOAD_PRELOAD_ENABLE +TIM3.IPParameters=Prescaler,Period,AutoReloadPreload,TIM_MasterSlaveMode,TIM_MasterOutputTrigger +TIM3.Period=999 +TIM3.Prescaler=83 +TIM3.TIM_MasterOutputTrigger=TIM_TRGO_UPDATE +TIM3.TIM_MasterSlaveMode=TIM_MASTERSLAVEMODE_ENABLE UART4.IPParameters=VirtualMode UART4.VirtualMode=Asynchronous UART5.IPParameters=VirtualMode @@ -690,7 +706,7 @@ VP_IWDG_VS_IWDG.Mode=IWDG_Activate VP_IWDG_VS_IWDG.Signal=IWDG_VS_IWDG VP_SYS_VS_tim7.Mode=TIM7 VP_SYS_VS_tim7.Signal=SYS_VS_tim7 -VP_TIM6_VS_ClockSourceINT.Mode=Enable_Timer -VP_TIM6_VS_ClockSourceINT.Signal=TIM6_VS_ClockSourceINT +VP_TIM3_VS_ClockSourceINT.Mode=Internal +VP_TIM3_VS_ClockSourceINT.Signal=TIM3_VS_ClockSourceINT board=custom rtos.0.ip=FREERTOS diff --git a/Src/adc.c b/Src/adc.c index cc5cf0d1..97621a1a 100644 --- a/Src/adc.c +++ b/Src/adc.c @@ -25,6 +25,7 @@ /* USER CODE END 0 */ ADC_HandleTypeDef hadc1; +DMA_HandleTypeDef hdma_adc1; /* ADC1 init function */ void MX_ADC1_Init(void) @@ -48,11 +49,11 @@ void MX_ADC1_Init(void) hadc1.Init.ScanConvMode = ENABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; - hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; - hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; + hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; + hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 9; - hadc1.Init.DMAContinuousRequests = DISABLE; + hadc1.Init.DMAContinuousRequests = ENABLE; hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; if (HAL_ADC_Init(&hadc1) != HAL_OK) { @@ -141,7 +142,15 @@ void MX_ADC1_Init(void) Error_Handler(); } /* USER CODE BEGIN ADC1_Init 2 */ - + /* STM32F4 ADC 上电**自校准**:RM0090 §13.3.6 / AN4976 要求的必需步骤,CubeMX 不生成, + * 且本仓库的 HAL 是裁剪子集、**没有** HAL_ADCEx_Calibration_Start()(已核实 + * stm32f4xx_hal_adc_ex.c/.h 均无该函数)→ 用本文件 USER CODE BEGIN 1 里的等价实现。 + * 必须在首次转换之前完成,否则 ADC 存在失调误差(未校准的偏移可达数十 LSB)。 + * 放 USER CODE 区 = L2,重生成不丢;由 cubemx-survival 审计项 adc1-calibration 兜底。 */ + if (adc1_calibrate() != 0) + { + Error_Handler(); + } /* USER CODE END ADC1_Init 2 */ } @@ -182,6 +191,25 @@ void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle) GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(ADC17_UB_GPIO_Port, &GPIO_InitStruct); + /* ADC1 DMA Init */ + /* ADC1 Init */ + hdma_adc1.Instance = DMA2_Stream4; + hdma_adc1.Init.Channel = DMA_CHANNEL_0; + hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; + hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; + hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; + hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; + hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; + hdma_adc1.Init.Mode = DMA_CIRCULAR; + hdma_adc1.Init.Priority = DMA_PRIORITY_LOW; + hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE; + if (HAL_DMA_Init(&hdma_adc1) != HAL_OK) + { + Error_Handler(); + } + + __HAL_LINKDMA(adcHandle,DMA_Handle,hdma_adc1); + /* USER CODE BEGIN ADC1_MspInit 1 */ /* USER CODE END ADC1_MspInit 1 */ @@ -215,6 +243,8 @@ void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle) HAL_GPIO_DeInit(ADC17_UB_GPIO_Port, ADC17_UB_Pin); + /* ADC1 DMA DeInit */ + HAL_DMA_DeInit(adcHandle->DMA_Handle); /* USER CODE BEGIN ADC1_MspDeInit 1 */ /* USER CODE END ADC1_MspDeInit 1 */ @@ -222,6 +252,64 @@ void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle) } /* USER CODE BEGIN 1 */ +/* CR2.CAL(bit 3)。⚠ 本仓库的 CMSIS 设备头 stm32f407xx.h 是裁剪版,**未定义** + * ADC_CR2_CAL(已核实:CR2 只有 ADON/CONT/DMA/DDS/EOCS/ALIGN/JEXTSEL/JEXTEN/ + * JSWSTART/EXTSEL/EXTEN/SWSTART,没有 CAL) + * → 按 RM0090 在此补齐,命名避开 CMSIS 前缀以免将来重定义冲突。 */ +#define ADC1_CAL_BIT (0x1UL << 3U) +/* 断电/校准等待上限(ms)。CAL 实测在数十 µs 内清零,10 ms 是 100 倍以上余量 */ +#define ADC1_CAL_TIMEOUT_MS (10U) +/* ADON 0→1 后的稳定等待(µs),同 HAL 的 ADC_STAB_DELAY_US 口径 */ +#define ADC1_STAB_DELAY_US (20U) +/* + * 函数功能:ADC1 上电自校准(等价于 HAL_ADCEx_Calibration_Start,本仓库 HAL 无此函数) + * 入口参数:无(操作 ADC1 寄存器) + * 返 回 值:0 - 校准完成;-1 - 等待超时(ADON 未清零 / CAL 未清零) + * 限定条件:MX_ADC1_Init() 之后、**首次转换之前**;内核启动前可调用 + * (只用 HAL_GetTick 与寄存器,不触碰任何 CMSIS/FreeRTOS 对象) + * 函数说明:RM0090 §13.3.6 时序——① ADC 必须处于关闭态(清 ADON 并等其生效); + * ② 置 ADON 上电,等 Tstab;③ 置 CAL 启动自校准;④ 完成后**硬件自动**清 CAL, + * 等其清零即完成。校准值存在 ADC 内部偏置寄存器,此后所有转换都带上校正。 + * 缺这一步的后果不是"读不到"而是"读得有偏"——正是 §7.2 那类 + * adc1_err/异常值的候选根因之一,见 docs/硬件平台/ADC通道映射说明.md。 + */ +int adc1_calibrate(void) +{ + uint32_t tickstart; + + /* 1) 前置条件:ADC 必须关闭 */ + CLEAR_BIT(ADC1->CR2, ADC_CR2_ADON); + tickstart = HAL_GetTick(); + while ((ADC1->CR2 & ADC_CR2_ADON) != 0U) + { + if ((HAL_GetTick() - tickstart) > ADC1_CAL_TIMEOUT_MS) + { + return -1; + } + } + + /* 2) 上电(ADON 0→1),等待稳定后再启动校准 */ + SET_BIT(ADC1->CR2, ADC_CR2_ADON); + { + __IO uint32_t wait_loop_index = (ADC1_STAB_DELAY_US * (SystemCoreClock / 1000000U)); + while (wait_loop_index != 0U) + { + wait_loop_index--; + } + } + + /* 3) 启动自校准,等硬件把 CAL 清零 */ + SET_BIT(ADC1->CR2, ADC1_CAL_BIT); + tickstart = HAL_GetTick(); + while ((ADC1->CR2 & ADC1_CAL_BIT) != 0U) + { + if ((HAL_GetTick() - tickstart) > ADC1_CAL_TIMEOUT_MS) + { + return -1; + } + } + return 0; +} /* USER CODE END 1 */ diff --git a/Src/dma.c b/Src/dma.c index 9b224df3..0404dcef 100644 --- a/Src/dma.c +++ b/Src/dma.c @@ -59,6 +59,9 @@ void MX_DMA_Init(void) /* DMA2_Stream3_IRQn interrupt configuration */ HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 7, 0); HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn); + /* DMA2_Stream4_IRQn interrupt configuration */ + HAL_NVIC_SetPriority(DMA2_Stream4_IRQn, 6, 0); + HAL_NVIC_EnableIRQ(DMA2_Stream4_IRQn); /* DMA2_Stream7_IRQn interrupt configuration */ HAL_NVIC_SetPriority(DMA2_Stream7_IRQn, 9, 0); HAL_NVIC_EnableIRQ(DMA2_Stream7_IRQn); diff --git a/Src/freertos.c b/Src/freertos.c index 5877bdf0..31d2fbe1 100644 --- a/Src/freertos.c +++ b/Src/freertos.c @@ -39,6 +39,8 @@ #include "sys_events.h" #include "default_task.h" #include "adc_task.h" /* B1(2026-10-10):ADC 采集消费者(手写任务,见 Variables 区) */ +#include "iec104_serv.h" /* B4(2026-10-12):IEC 104 上行服务(手写任务,见 Variables 区) */ +#include "protect_task.h" /* B5(2026-10-12):保护判据节拍任务(手写任务,同上) */ /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ @@ -113,10 +115,19 @@ static int hook_ptr_ok(const void *p) * 靠审计 WARN 提示),建监听 → net_bind → CH395F → 日志尾。2026-10-10 整改后 * 静态最坏链 1024 B(整改前 1736 B)、实测峰值 616 B;⚠ 余量 1024 B **正好** * 等于 ISR 底线 → 本任务链上再加任何调用(或新日志行)都必须先抬栈。 - * adcTask 512 words:本区下方手工定义(同上,不在 `.ioc` 里)。B1(2026-10-10) - * ADC 采集消费者:链上仅 tpafe5160_start_conv_irq + tpafe5160_snapshot(临界区 - * 原子拷贝)+ task_wd_beat + 周期 DBG_INFO(每 10s 一条)。栈值以 - * `scripts/stack_report.py` 实测最坏链为准,改动前后各跑一次。 + * adcTask 640 words:本区下方手工定义(同上,不在 `.ioc` 里)。B1(2026-10-10) + * ADC 采集消费者;L2 相量(2026-10-11)+ L1-6 有效性校验(同日)后链上为 + * adc_input_poll + sample_ring/phasor + adc_health_report(DBG_ERROR + + * syslog_post 回显尾)。静态最坏链 1080 B,512 words 时余 968 B < 1024 B + * ISR 底线 → 抬至 640 words(2.5 KB,堆余量 min_ever 7152→≈6640 B,可承受)。 + * 栈值以 `scripts/stack_report.py` 实测最坏链为准,改动前后各跑一次。 + * iec104Task 640 words:B4(2026-10-12)104 上行服务(2404 监听 + link 状态 + * 机)。link/pts 缓冲全静态;stack_report 实测静态最坏链 1456 B(拆链 + * 重建监听 → net_bind → SPI 故障 → 日志尾,日志主导)。512 words 时余 + * 592 B < 1024 B ISR 底线 → 抬至 640(2.5 KB)。 + * protectTask 512 words:B5(2026-10-12)保护判据节拍(prot_elm_run 消费者 + * + L5-4 采样闭锁)。stack_report 实测静态最坏链 872 B(float 判据 + + * phasor 快照 + 日志尾),余 1176 B / 57% ≥ ISR 底线 → 512 保持。 */ /* 存储测试变量(g_storage_stats / s_perf_buf)已迁至 test/board/storage_test_task.c */ osThreadId_t timeSyncTaskHandle; @@ -125,11 +136,38 @@ const osThreadAttr_t timeSyncTask_attributes = { .stack_size = 512 * 4, .priority = (osPriority_t) osPriorityNormal, }; -/* B1(2026-10-10):ADC 采集消费者(1ms 采样节拍 + 原子快照 + 帧统计)。 - * 手写于此而非 .ioc(同 timeSyncTask:不在 Tasks01 里 → 重生成会丢,靠审计 WARN 提示)。 */ +/* B1(2026-10-10):ADC 采集消费者(TIM6 1 kHz 采样节拍 + 原子快照 + 帧统计)。 + * 手写于此而非 .ioc(同 timeSyncTask:不在 Tasks01 里 → 重生成会丢,靠审计 WARN 提示)。 + * ⚠ 优先级 AboveNormal(Task #32 P1-5 方案 a,2026-10-10):消费改由 TIM6 ISR 事件驱动后 + * 提优先级,使"被唤醒 → 立即抢占其余 Normal 任务",唤醒延迟固定为 µs 级。 + * 代价:本任务单拍最坏实测 adcm≈2.1 ms 会在 AboveNormal 上挤占 net/ftp/default/timeSync, + * 其中 ADC 的 2 ms 忙等(App/algo/adc_input.c)尤其敏感 → 硬化方向见 + * docs/子系统设计/ADC采样节拍设计说明.md §10.4。 */ osThreadId_t adcTaskHandle; const osThreadAttr_t adcTask_attributes = { .name = "adcTask", + .stack_size = 640 * 4, + .priority = (osPriority_t) osPriorityAboveNormal, +}; +/* B4(2026-10-12):IEC 60870-5-104 被控站服务(2404 监听 + 链路状态机消费)。 + * 手写于此而非 .ioc(同 timeSyncTask/adcTask:不在 Tasks01 里 → 重生成会丢, + * 靠审计 WARN 提示)。640 words:stack_report 实测静态最坏链 1456 B(拆链→ + * 重建监听→net_bind→SPI 故障→日志尾,日志主导);512 words 时余 592 B + * < 1024 B ISR 底线 → 抬至 640(2.5 KB,CCM 堆 min_ever 再降 512 B,可承受)。 */ +osThreadId_t iec104TaskHandle; +const osThreadAttr_t iec104Task_attributes = { + .name = "iec104Task", + .stack_size = 640 * 4, + .priority = (osPriority_t) osPriorityNormal, +}; +/* B5(2026-10-12):保护判据节拍(prot_elm_run 消费者 + L5-4 采样闭锁)。 + * 手写于此而非 .ioc(同上)。512 words:stack_report 实测静态最坏链 872 B + * (float 判据 + phasor 快照 + 日志尾),余 1176 B / 57% ≥ ISR 底线 → 保持。 + * 优先级 Normal:V0.1 无出口硬件(规划 R7),判据是 20ms 软实时,不需要 + * adcTask 的 AboveNormal;规划 §4 的"1ms/高"随 P2 瞬时值元件再评。 */ +osThreadId_t protectTaskHandle; +const osThreadAttr_t protectTask_attributes = { + .name = "protectTask", .stack_size = 512 * 4, .priority = (osPriority_t) osPriorityNormal, }; @@ -313,6 +351,12 @@ void MX_FREERTOS_Init(void) { * (内核启动前)完成;本任务只做运行期采样。 */ adcTaskHandle = osThreadNew(adc_task_func, NULL, &adcTask_attributes); + /* B4(2026-10-12):IEC 60870-5-104 上行服务任务。任务体内先等 NET_READY + * 屏障(100ms 分片 + 每片喂心跳,timeSyncTask 先例),再建 2404 监听并 + * 100ms 循环服务;纳入 IWDG required_mask(TASK_WD_IEC104,任何构建都创建)。 */ + iec104TaskHandle = osThreadNew(iec104_serv_task, NULL, &iec104Task_attributes); + protectTaskHandle = osThreadNew(protect_task, NULL, &protectTask_attributes); + /* P2-13(2026-10-04):产品构建(未选任何 TEST_SUITE_*)不需要 testTask。 * 上方生成区按 .ioc FREERTOS.Tasks01 无条件创建它,其 2048 words(8KB) 栈 + TCB * 取自 CCM 堆;而产品构建下 start_test_task() 仅打印一行后 osDelay(1000) 永久空转, diff --git a/Src/main.c b/Src/main.c index 3881ad93..3829c6a7 100644 --- a/Src/main.c +++ b/Src/main.c @@ -42,6 +42,7 @@ #include "app_main.h" #include "sys_clock.h" #include "task_watchdog.h" /* task_wd_tick_probe():round 8b 时间源存活性取证 */ +#include "adc_task.h" /* adc_tick_irq_notify():TIM6 节拍 → 唤醒 adcTask(P1-5 方案 a) */ /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ @@ -56,6 +57,11 @@ /* Private macro -------------------------------------------------------------*/ /* USER CODE BEGIN PM */ +/* N-20(2026-10-12):本文件唯一的 DBG_* 调用点是 Error_Handler;其帧经 + * MX_*_Init 调用链叠在 MSP 上,而 MSP 仅 1KB(startup Stack_Size 0x400) + * → 行缓冲收紧到 128 B(本文件最长行 = Error_Handler 行 ≈78 B < 128,不截断)。 + * 日志成本口径见 dbg_log.h 的 DBG_LINE_BUF_MAX 注释。 */ +#define DBG_LINE_BUF_MAX 128U #define DBG_TAG "[MAIN]" #include "dbg_log.h" /* USER CODE END PM */ @@ -132,7 +138,7 @@ int main(void) MX_USART3_UART_Init(); MX_IWDG_Init(); MX_ADC1_Init(); - MX_TIM6_Init(); + MX_TIM3_Init(); /* USER CODE BEGIN 2 */ HAL_Delay(100); @@ -246,13 +252,22 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) task_wd_tick_probe(); } - /* 1 kHz 采样节拍:TIM6 更新中断内脉冲 CONVST(仅两次 GPIO 写,亚 µs)。 - * TIM6 与 BUSY EXTI(EXTI9_5_IRQn) 同为抢占优先级 6 → 互不抢占, + /* 1 kHz 采样节拍:TIM3 更新事件内脉冲 CONVST(仅两次 GPIO 写,亚 µs), + * 再唤醒 adcTask 消费这一拍(Task #32 P1-5 方案 a:ISR 只发唤醒,计算全留在任务)。 + * 同一个更新事件还经 TRGO 硬件触发 STM32 ADC1 一轮 9 通道转换(DMA 自动搬运) + * → 两片转换器由同一硬件事件驱动,采样时刻对齐。 + * TIM3 与 BUSY EXTI(EXTI9_5_IRQn) 同为抢占优先级 6 → 互不抢占, * 读 RD 时序不会被下一次 CONVST 脉冲打断。详见 - * docs/子系统设计/ADC采样节拍设计说明.md。放 USER CODE 区(L2)。 */ - if (htim->Instance == TIM6) + * docs/子系统设计/ADC采样节拍设计说明.md §10。放 USER CODE 区(L2)。 + * ⚠ 节拍源 v2.0 由 TIM6 改为 TIM3:F4 上 ADC1 的外部触发清单里没有 TIM6 TRGO + * (TIM6/TIM7 的 TRGO 只接 DAC),详见 App/app/app_main.c 步骤 6 注释。 + * ⚠ 唤醒必须走 adc_tick_irq_notify(),**不要**在这里直接 osThreadFlagsSet: + * TIM3 在 app_main_init() 就启动,远早于建任务与 osKernelStart, + * "消费者就绪"门在 adc_task.c 里(同本文件 TIM7 分支 2026-10-02 的实测坑)。 */ + if (htim->Instance == TIM3) { tpafe5160_start_conv_irq(); + adc_tick_irq_notify(); } /* USER CODE END Callback 1 */ } @@ -264,11 +279,26 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) void Error_Handler(void) { /* USER CODE BEGIN Error_Handler_Debug */ - /* User can add his own implementation to report the HAL error return state */ - __disable_irq(); - while (1) - { - } + /* N-20 统一降级(2026-10-12):旧形态 __disable_irq()+while(1) = 单处外设初始化 + * 失败即整机挂死,只剩 IWDG 兜底且无任何自检日志。现:打 1 条自检日志 + + * NVIC_SystemReset()(与 4 个 Fault 处理同款自恢复;IWDG 是最后兜底而非唯一 + * 出路;复位归因 SFTRSTF → 开机日志 "software (NVIC_SystemReset)")。 + * ⚠ 不得 __disable_irq():日志必须能走串口。 + * 调用方识别(为何不打地址):本函数无参数、调用点全在 CubeMX 生成区(不可加标签); + * ⚠ __builtin_return_address(0) **不可用**——AC5 前端接受但编成库函数调用, + * MicroLIB 无该符号 → 链接错 L6218E(2026-10-12 台架实证,勿再尝试); + * ARMCC __asm 只有整函数形态且取不到 C 符号、带不动 C 日志(看门狗设计说明 §10.4)。 + * → 子系统定位靠启动日志锚点:失败点在"最后一条成功日志"与 main() 的 + * MX_*_Init 顺序(GPIO→DMA→USART1→SPI2→SPI1→I2C1→UART4→UART5→USART2→ + * USART3→IWDG→ADC1→TIM3→app_main_init)之间。 + * 日志路径内核前安全:调用点全部在 osKernelStart 之前,控制台写者未注册 + * → con_append 走 dbg_console_write_raw(DWT/计数有界轮询,huart1 未就绪 + * 即返回),sys_clock_get_str 未初始化时打占位串——全程无内核 API。 + * 日志成本:仅这 1 条(≈90 B Flash;行缓冲已在本文件 PM 区收到 128 B)。 + * 重生成存活:函数体在 USER CODE Error_Handler_Debug 区(L2,KeepUserCode + * 逐字保留);另登记 check_cubemx_survival.py 审计项兜底(L3)。 */ + DBG_ERROR("boot init failure, resetting (locate MX_*_Init via boot order)"); + NVIC_SystemReset(); /* USER CODE END Error_Handler_Debug */ } #ifdef USE_FULL_ASSERT diff --git a/Src/stm32f4xx_it.c b/Src/stm32f4xx_it.c index bb0d0ac4..f898764c 100644 --- a/Src/stm32f4xx_it.c +++ b/Src/stm32f4xx_it.c @@ -238,13 +238,14 @@ __asm void UsageFault_Handler(void) /* USER CODE END 0 */ /* External variables --------------------------------------------------------*/ +extern DMA_HandleTypeDef hdma_adc1; extern DMA_HandleTypeDef hdma_spi1_rx; extern DMA_HandleTypeDef hdma_spi1_tx; extern DMA_HandleTypeDef hdma_spi2_rx; extern DMA_HandleTypeDef hdma_spi2_tx; extern SPI_HandleTypeDef hspi1; extern SPI_HandleTypeDef hspi2; -extern TIM_HandleTypeDef htim6; +extern TIM_HandleTypeDef htim3; extern DMA_HandleTypeDef hdma_usart1_tx; extern DMA_HandleTypeDef hdma_usart1_rx; extern UART_HandleTypeDef huart4; @@ -398,6 +399,20 @@ void EXTI9_5_IRQHandler(void) /* USER CODE END EXTI9_5_IRQn 1 */ } +/** + * @brief This function handles TIM3 global interrupt. + */ +void TIM3_IRQHandler(void) +{ + /* USER CODE BEGIN TIM3_IRQn 0 */ + + /* USER CODE END TIM3_IRQn 0 */ + HAL_TIM_IRQHandler(&htim3); + /* USER CODE BEGIN TIM3_IRQn 1 */ + + /* USER CODE END TIM3_IRQn 1 */ +} + /** * @brief This function handles SPI1 global interrupt. */ @@ -496,20 +511,6 @@ void UART5_IRQHandler(void) /* USER CODE END UART5_IRQn 1 */ } -/** - * @brief This function handles TIM6 global interrupt, DAC1 and DAC2 underrun error interrupts. - */ -void TIM6_DAC_IRQHandler(void) -{ - /* USER CODE BEGIN TIM6_DAC_IRQn 0 */ - - /* USER CODE END TIM6_DAC_IRQn 0 */ - HAL_TIM_IRQHandler(&htim6); - /* USER CODE BEGIN TIM6_DAC_IRQn 1 */ - - /* USER CODE END TIM6_DAC_IRQn 1 */ -} - /** * @brief This function handles TIM7 global interrupt. */ @@ -566,6 +567,20 @@ void DMA2_Stream3_IRQHandler(void) /* USER CODE END DMA2_Stream3_IRQn 1 */ } +/** + * @brief This function handles DMA2 stream4 global interrupt. + */ +void DMA2_Stream4_IRQHandler(void) +{ + /* USER CODE BEGIN DMA2_Stream4_IRQn 0 */ + + /* USER CODE END DMA2_Stream4_IRQn 0 */ + HAL_DMA_IRQHandler(&hdma_adc1); + /* USER CODE BEGIN DMA2_Stream4_IRQn 1 */ + + /* USER CODE END DMA2_Stream4_IRQn 1 */ +} + /** * @brief This function handles DMA2 stream7 global interrupt. */ diff --git a/Src/tim.c b/Src/tim.c index aa07ef25..1ebeb097 100644 --- a/Src/tim.c +++ b/Src/tim.c @@ -24,78 +24,85 @@ /* USER CODE END 0 */ -TIM_HandleTypeDef htim6; +TIM_HandleTypeDef htim3; -/* TIM6 init function */ -void MX_TIM6_Init(void) +/* TIM3 init function */ +void MX_TIM3_Init(void) { - /* USER CODE BEGIN TIM6_Init 0 */ + /* USER CODE BEGIN TIM3_Init 0 */ - /* USER CODE END TIM6_Init 0 */ + /* USER CODE END TIM3_Init 0 */ + TIM_ClockConfigTypeDef sClockSourceConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; - /* USER CODE BEGIN TIM6_Init 1 */ + /* USER CODE BEGIN TIM3_Init 1 */ - /* USER CODE END TIM6_Init 1 */ - htim6.Instance = TIM6; - htim6.Init.Prescaler = 83; - htim6.Init.CounterMode = TIM_COUNTERMODE_UP; - htim6.Init.Period = 999; - htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; - if (HAL_TIM_Base_Init(&htim6) != HAL_OK) + /* USER CODE END TIM3_Init 1 */ + htim3.Instance = TIM3; + htim3.Init.Prescaler = 83; + htim3.Init.CounterMode = TIM_COUNTERMODE_UP; + htim3.Init.Period = 999; + htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; + htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; + if (HAL_TIM_Base_Init(&htim3) != HAL_OK) { Error_Handler(); } - sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; - sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; - if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK) + sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; + if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK) { Error_Handler(); } - /* USER CODE BEGIN TIM6_Init 2 */ + sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; + sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_ENABLE; + if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN TIM3_Init 2 */ - /* USER CODE END TIM6_Init 2 */ + /* USER CODE END TIM3_Init 2 */ } void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle) { - if(tim_baseHandle->Instance==TIM6) + if(tim_baseHandle->Instance==TIM3) { - /* USER CODE BEGIN TIM6_MspInit 0 */ + /* USER CODE BEGIN TIM3_MspInit 0 */ - /* USER CODE END TIM6_MspInit 0 */ - /* TIM6 clock enable */ - __HAL_RCC_TIM6_CLK_ENABLE(); + /* USER CODE END TIM3_MspInit 0 */ + /* TIM3 clock enable */ + __HAL_RCC_TIM3_CLK_ENABLE(); - /* TIM6 interrupt Init */ - HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 6, 0); - HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn); - /* USER CODE BEGIN TIM6_MspInit 1 */ + /* TIM3 interrupt Init */ + HAL_NVIC_SetPriority(TIM3_IRQn, 6, 0); + HAL_NVIC_EnableIRQ(TIM3_IRQn); + /* USER CODE BEGIN TIM3_MspInit 1 */ - /* USER CODE END TIM6_MspInit 1 */ + /* USER CODE END TIM3_MspInit 1 */ } } void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* tim_baseHandle) { - if(tim_baseHandle->Instance==TIM6) + if(tim_baseHandle->Instance==TIM3) { - /* USER CODE BEGIN TIM6_MspDeInit 0 */ + /* USER CODE BEGIN TIM3_MspDeInit 0 */ - /* USER CODE END TIM6_MspDeInit 0 */ + /* USER CODE END TIM3_MspDeInit 0 */ /* Peripheral clock disable */ - __HAL_RCC_TIM6_CLK_DISABLE(); + __HAL_RCC_TIM3_CLK_DISABLE(); - /* TIM6 interrupt Deinit */ - HAL_NVIC_DisableIRQ(TIM6_DAC_IRQn); - /* USER CODE BEGIN TIM6_MspDeInit 1 */ + /* TIM3 interrupt Deinit */ + HAL_NVIC_DisableIRQ(TIM3_IRQn); + /* USER CODE BEGIN TIM3_MspDeInit 1 */ - /* USER CODE END TIM6_MspDeInit 1 */ + /* USER CODE END TIM3_MspDeInit 1 */ } } diff --git a/docs/README.md b/docs/README.md index 7cc7d007..cca2ac08 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,7 +11,7 @@ ``` docs/ ├── README.md 本索引(阅读顺序 + 全量清单) -├── 待处理内容.md 统一待办登记簿(高频入口,保留在顶层) +├── 任务规划/ 统一待办登记簿(README 排序权威 + 6 个拆分文件) ├── 产品需求/ 产品需求设计文档(面向产品的需求基线) ├── 架构设计/ 全局架构与设计方法论 ├── 子系统设计/ 各功能子系统的设计说明 @@ -53,7 +53,7 @@ docs/ - **应用/驱动开发** → §4 架构设计 → §5 子系统设计 → §7 问题记录(**改驱动前必读对应 Trap Records**) - **测试/验证** → §6 测试说明 → §9.2 环境与部署 - **UI 开发** → §8 UI 设计 → §6 测试说明(串口助手、UI 自动化测试流程) -- **接手/评审** → §9.1 评审报告 → §9.3 待处理内容(统一待办登记簿) +- **接手/评审** → §9.1 评审报告 → §9.3 任务规划(统一待办登记簿,见 `任务规划/README.md`) --- @@ -88,8 +88,9 @@ docs/ | [`系统墙钟时间维护说明.md`](子系统设计/系统墙钟时间维护说明.md) | `sys_clock`:RTC 基准、tick 回绕防护、30 天惰性再基准化 | | [`控制台单写者日志设计说明.md`](子系统设计/控制台单写者日志设计说明.md) | 控制台日志单写者模型(避免多任务交错输出) | | [`运行与自检记录设计说明.md`](子系统设计/运行与自检记录设计说明.md) | `/log/run.log`·`selftest.log` 定长记录与待机显示 | -| [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | IWDG(CubeMX 配置)+ 任务心跳位图:选型与超时推导、掩码与埋点、取证机制、验证记录;已台架闭环(V2/V4/V5) | -| [`ADC采样节拍设计说明.md`](子系统设计/ADC采样节拍设计说明.md) | **ADC 采集链路采样节拍**:现状(任务周期软触发)→ 目标(TIM6 更新中断触发 CONVST)的方案选型、CubeMX 配置规格、代码改造清单、时序/优先级分析与验收判据;**规划中(未实现)** | +| [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | IWDG(CubeMX 配置)+ 任务心跳位图:选型与超时推导、掩码与埋点、取证机制、验证记录;已台架闭环(V2/V4/V5)。**§11 = 任务级饥饿的诊断方法与 `[PROF]` 实测基线**(Task #32 沉淀:判读口径、量化归因、三条脆弱点、明确不做的四条) | +| [`ADC采样节拍设计说明.md`](子系统设计/ADC采样节拍设计说明.md) | **ADC 采集链路采样节拍**:TIM3 1 kHz 更新事件同时触发 TPAFE CONVST 与(经 TRGO)STM32 ADC1 + DMA 乒乓缓冲;方案选型、CubeMX 配置规格、代码改造清单、时序/优先级分析、验收判据与实测剖面;**已落地并上板闭环** | +| [`IEC104上行设计说明.md`](子系统设计/IEC104上行设计说明.md) | **B4 上行规约**:IEC 60870-5-104 被控站(TCP 2404)范围/点表(V0.1 有效子集)/链路流程(k/w/t1~t3、总召、COV、遥控两段式、定值)/netTask 接入决策/预算与测试方案;协议核心纯逻辑 + L0.5 全栈单测 | | [`网络/BSD_Socket_API_使用指南.md`](网络/BSD_Socket_API_使用指南.md) | **上层视图**:BSD Socket API 公开语义、示例、常见问题 | | [`网络/BSD_Socket_API_函数调用关系.md`](网络/BSD_Socket_API_函数调用关系.md) | **内部视图**:netTask / 消息队列 / L3 挂起 / 重试对账的调用关系与设计意图 | | [`网络/Net层阻塞模型架构设计.md`](网络/Net层阻塞模型架构设计.md) | **等待模型**:pending 请求架构、五路完成语义、设计边界与性能特征 | @@ -124,7 +125,11 @@ docs/ |---|---| | [`看门狗单元测试说明.md`](测试说明/看门狗单元测试说明.md) | Task Watchdog 判定状态机(位图 / 掩码 / 节流 / tick 回绕),39 用例 | | [`系统时钟单元测试说明.md`](测试说明/系统时钟单元测试说明.md) | sys_clock 时间换算 / 星期 Zeller / 30 天惰性再基准化 / RTC 故障降级,100 断言点 | -| [`配置文件单元测试说明.md`](测试说明/配置文件单元测试说明.md) | cfg_file INI 解析 / 值编解码 / load-save-poll 契约 / 超长守卫,139 断言点(含抓出的外科手术保存死码 P0) | +| [`配置文件单元测试说明.md`](测试说明/配置文件单元测试说明.md) | cfg_file INI 解析 / 值编解码 / load-save-poll 契约 / 超长守卫,149 断言点(含抓出的外科手术保存死码 P0) | +| [`ADC输入层单元测试说明.md`](测试说明/ADC输入层单元测试说明.md) | adc_input 语义通道映射 / ADC1 顺序扫描(**Rank↔通道判据**)/ poll 契约 / HAL 故障路径,70 用例 | +| [`相量层单元测试说明.md`](测试说明/相量层单元测试说明.md) | phasor 全周傅氏系数表 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 双区交换,57 用例 | +| [`卷锁单元测试说明.md`](测试说明/卷锁单元测试说明.md) | vol_lock 创建 / 获取 / 释放契约(**fail-open 不变量**:创建失败仍返回 1 且零阻塞)/ 幂等 / 超时透传,33 断言点 | +| [`时间同步单元测试说明.md`](测试说明/时间同步单元测试说明.md) | time_sync 分片喂狗延时 / 监听退避 / 时间包重组 / 任务主循环(**N-17 的全部逻辑贡献者**),80 断言点 | **模块测试指南**(动手写测试前先读) @@ -172,6 +177,7 @@ docs/ | [`RAM使用分析与优化方案.md`](内存管理/RAM使用分析与优化方案.md) | **内存主文档**:占用分析、优化方案、定栈规则 | | [`内存内容分配策略.md`](内存管理/内存内容分配策略.md) | 各内存区(SRAM/CCM)的分配策略 | | [`存储栈RAM优化调研.md`](内存管理/存储栈RAM优化调研.md) | 存储栈 RAM 优化调研与结论 | +| [`RAM预算台账.md`](内存管理/RAM预算台账.md) | **预算台账**:子系统配额、CCM 分配、趋势基线与 92% 门禁(N-27 三件套,2026-10-12) | | `data/` | `ram_report_*.md` / `stack_report_*.md` 快照数据(由 `scripts/` 生成) | ### 7.3 问题记录(**改驱动前必读**) @@ -207,19 +213,20 @@ docs/ | 文档 | 内容 | |---|---| -| [`问题记录/项目审查与优化建议报告.md`](问题记录/项目审查与优化建议报告.md) | 项目全景审查:文档/代码/架构/可靠性问题清单(D-1~D-7、N-xx、Px-xx)——**体系层评审报告,非待办池**(待办见 §9.3 `待处理内容.md`) | +| [`问题记录/项目审查与优化建议报告.md`](问题记录/项目审查与优化建议报告.md) | 项目全景审查:文档/代码/架构/可靠性问题清单(未闭环项:D-1、N-xx、Px-xx;**已修复项按「修复即销项」删除**,去处见其变更记录)——**体系层评审报告,非待办池**(待办见 `任务规划/README.md`) | ### 9.2 环境与部署 | 文档 | 内容 | |---|---| | [`项目部署环境要求.md`](环境与部署/项目部署环境要求.md) | 开发机软件环境、目标板资源预算、现场部署与验收步骤 | +| [`并行任务工作树方案.md`](环境与部署/并行任务工作树方案.md) | **多任务并行开发机制**:`git worktree` 隔离、目录与共享/独占分配(junction 规则)、`task-worktree`(`@task`)SKILL 命令面、单写者文档与合并残留审计、台架锁;**已落地并端到端实测**(见其 §11) | ### 9.3 台账与外部资料 | 位置 | 内容 | |---|---| -| [`待处理内容.md`](待处理内容.md) | **统一待办登记簿**——审计缺陷(P-x)+ 台架/审计观察(N/S/T-x)+ 测试与架构专项(S1x/A-x)+ 任务排序(阶段 A/B/C/D)+ 诊断基线;只保留未闭环项,完成即删 | +| [`任务规划/`](任务规划/README.md) | **统一待办登记簿(文件夹)**——`README.md` = 排序权威(波次 W1~W7)+ 编号空间 + 维护约定;`W1-采集保护收尾.md` = 当前波次任务卡(里程碑 × 并行道 × 五字段);`登记簿.md` = N/S/T-x 长期观察;`排期详述.md` / `暂缓-RS485.md` / `测试与架构专项.md` / `基线与待确认.md` = 波内详表 / 冻结簇 / 专项 / 基线;只保留未闭环项,完成即删 | | `硬件平台/` | 网表详细分析报告与语义图(V0.0 / V0.1);**[`ADC通道映射说明.md`](硬件平台/ADC通道映射说明.md)——V0.1 十七路 ADC 语义/网名/通道号权威映射表(固件取数、UI 显示、自检注入以此为准)** | | `芯片手册/` | 外部芯片数据手册转录(CH395F / GD5F2GQ5UE / SD2506 / TPAFE5160 / RTL8305NBI 等) | | `images/` | 文档技术配图(`.svg` 产物,由 `tools/arch_diagram/`、`tools/refgraph/` 生成) | @@ -242,19 +249,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 标注「已删除」状态词) | -| 产物缺失 | `内存管理/RAM使用分析与优化方案.md` → `scripts/ram_baseline.tsv` | 方案写了、工具未建(引用已加 `[计划]` 标记) | +| 指向未实现工具 | `UI_界面制作流程.md`、`UI_自动化测试流程.md` → `tools/ui_mock/` | 该目录**始终不存在**;golden 回归实际落在 `simulator/tests/`(四脚本)。引用待改指,见 `docs/问题记录/项目审查与优化建议报告.md` §6.1 | +| ~~产物缺失~~ | ~~`内存管理/RAM使用分析与优化方案.md` → `scripts/ram_baseline.tsv`~~ | ✅ 已建(2026-10-12,N-27 前半):`ram_baseline.py` + `ram_baseline.tsv` + [`RAM预算台账.md`](内存管理/RAM预算台账.md) + `mem-budget` 钩子 | | ~~目录名含空格~~ | ~~`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、语义断链、元信息漂移)。 --- @@ -262,9 +269,12 @@ docs/ | 日期 | 变更 | 说明 | |---|---|---| +| 2026-10-12 | 新增环境与部署 | 新建 `docs/环境与部署/并行任务工作树方案.md`(**多任务并行开发机制规格**:`git worktree` 隔离、目录与共享/独占分配、`task-worktree` / `@task` skill 设计、单写者文档与合并残留审计、台架锁;**已落地并端到端实测**);本索引 §9.2 补登记 | +| 2026-10-11 | 功能 | N-19 FTP 鉴权:`/config.ini` 新增 `[ftp]` 段(`user` / `pwd_hash` = SHA-256(user‖password) / `write`)+ 恒定时间比较 + 连续 5 次失败锁 60 s + 未登录命令门;出厂默认 admin/admin,默认口令在用有启动告警。新增 `App/util/sha256.*`、`Drivers/BSP/NETCFG/ftp_cfg.*`、`Drivers/BSP/NET/lftpd/lftpd_auth.*`;主机侧单测 `test/host/sha256/`(13 例)+ `test/host/ftp_auth/`(36 例),pre-commit 钩子 26→28;台架脚本 `test/host/ftp_auth_bench.py`。文档同步:`docs/子系统设计/网络配置说明.md`、`docs/架构设计/STM32F4-Base存储架构说明.md`、`docs/代码规范/pre-commit使用说明.md`、`docs/产品需求/产品需求设计文档.md` | +| 2026-10-11 | 文档调整 | `AGENTS编写指南.md` 新建后**同日删除**(要点已沉淀入 `AGENTS.md` 宏观重构与本索引变更记录,指南类内容不单独成文);撤销 §1 目录树与 §7.1b 登记;同日 `AGENTS.md` 完成宏观重构(补项目概况/目录地图、平台约束分组、边界块) | | 2026-10-10 | 新增子系统设计 | 新建 `docs/子系统设计/ADC采样节拍设计说明.md`(ADC 采集链路 1 kHz 硬件定时器触发的 CubeMX 配置规格与代码改造清单,B1 遗留项规划);本索引 §5.1 补登记 | | 2026-10-10 | 新增算法专题 | 新建 `docs/算法/继电保护功能复现规划.md`(保护链路复现总规划);本索引 §1 目录树补 `算法/`、§5 拆为 5.1 功能子系统 / 5.2 算法专题并登记 `算法/` 两篇文档 | -| 2026-10-05 | 新增产品需求文档 | 新建 `docs/产品需求/产品需求设计文档.md`(面向产品的需求基线);本索引新增 §3 产品需求、§2 推荐阅读顺序置顶、§1 目录树补 `产品需求/`;后续章节号顺延(架构设计 §4 … 变更记录 §12) | +| 2026-10-05 | 新增产品需求文档 | 新建 `docs/产品需求/产品需求设计文档.md`(面向产品的需求基线);本索引新增 §3 产品需求、§2 推荐阅读顺序置顶、§1 目录树补 `产品需求/`;后续章节号顺延(本文 §4 … 本文 §12) | | 2026-09-25 | 目录重组 + 索引建立 | 顶层 16 篇散落 md 归入 5 个新目录(`架构设计/` `子系统设计/` `网络/` `审查记录/` `环境与部署/`);全仓库 90 处引用同步;新建本索引(D-5) | | 2026-09-25 | D-6 目录去空格 | `docs/UI 设计/` → `docs/UI设计/`;全仓库 67 处引用同步(含 URL 编码形态 `UI%20设计`);命名约定写入 §10 | | 2026-09-25 | D-7 文档-代码一致性门禁 | 新建 `scripts/check_doc_links.py`(链接存在性,含行内代码豁免/就近状态词/`[计划]` 标记)、`scripts/gen_task_table.py`(任务表由 `Src/freertos.c` 生成/校验,锚点 `TASK_TABLE:BEGIN/END`);接入 pre-commit `doc-links`/`task-table` 钩子;全库 66 文档 0 失效 | diff --git a/docs/UI设计/RemoDispBus协议与接收设计说明.md b/docs/UI设计/RemoDispBus协议与接收设计说明.md index 46c53abd..cfab465a 100644 --- a/docs/UI设计/RemoDispBus协议与接收设计说明.md +++ b/docs/UI设计/RemoDispBus协议与接收设计说明.md @@ -239,6 +239,23 @@ if (n > 0) { 2. **最小帧长判据统一**:`remo_handle` 以 `n < 5` 拦截(最短合法帧 5 字节),两端一致——旧 MCU `n < 6` 会丢 5 字节 KEEPLIVE/INIT 帧的隐患已随统一消除。 3. **断线重连统一**:`ui_net_sink_poll` 在客户端断开后用 `net_get_sock(g_listen) == NULL` 判监听是否失效(MCU 单连接模型下监听随客户端一起失效),失效即 `bind_listen()` 重建——两端一致,并修复了 MCU 单连接模型断线后无法重连的隐患。 4. **协议兼容**:线格式与命令语义不变,任一端都能与 DTU-RemoteLCD 参考客户端互通;双端行为一致已由 e2e 回归(`simulator/tests/check_remo_sink.py`,0/25600 位差异)与双端构建(Keil + CMake 均 0 错误 0 警告)验证。 +5. **驱动者是宿主,不是协议自己**:`ui_net_sink_poll()` 由宿主周期调用——MCU 端目前 = **uiTask 主循环 10 ms**,模拟器 = `main.c` 主循环。协议本身不含节拍、不阻塞。 + +> ✅ **门控已落地(2026-10-10)**:`ui_net_sink_poll()` 现在**先查后动**——`net_can_accept()` / +> `net_can_read()` 纯 RAM 读控制块判"有没有活",无活直接返回,不再每 10 ms 发一次 +> `net_accept`(原 100 msg/s 队列压力,与 FTP 抢深度 8 的队列)。门控判"无数据"时仍 +> **每 32 轮无条件 recv 一次**兜底(`readable` 只由 RECV_OK 中断置位,中断丢失时纯门控 +> 会把数据永久藏住)。模拟器后端恒返回"就绪",行为逐位不变。 +> +> ⚠ **残留暴露(已知,刻意不处理)**:门控只消除"无谓的往返",消除不了"往返本身无上界"—— +> 客户端在连且 netTask 拥塞时,3204 B 的 LCDMEM 应答仍可能让宿主任务等最长 ≈4 s。 +> "把推流搬出 uiTask(独立任务 + 帧 seqlock)"已完整评估并**决定不做**:实测刷新最坏周期 +> (`uip` ≈61 ms)是 **LVGL 帧周期地板**、与网络无关,搬任务不会让刷新变快;成本却含 +> **必改 `.ioc`** 的重生成与全套回归,收益只剩极端并发下的观感隔离。若日后它成为实际问题, +> 正确方向是先治 netTask 排队公平性(登记簿 N-4 二期),不是把 UI 搬走。 +> 观测手段:`[PROF]` 的 `uis`(往返最坏)与 `uisn`(此刻仍在推流段内多久)—— +> **`uisn` 持续非 0 就是这一格真发生的铁证**;字段语义与判读见 +> [`../子系统设计/看门狗设计说明.md`](../子系统设计/看门狗设计说明.md) §11.4。 --- diff --git a/docs/UI设计/UI_界面制作流程.md b/docs/UI设计/UI_界面制作流程.md index 3f4bd9da..1d82867a 100644 --- a/docs/UI设计/UI_界面制作流程.md +++ b/docs/UI设计/UI_界面制作流程.md @@ -1,7 +1,7 @@ # UI 界面制作流程(参考图 → LVGL) > **用途**:规范"参考图 → 设计规格 → LVGL 代码 → 双端验证"的完整管线,规定每一步的**动作、产物、放行条件(gate)**,保证过程可重复、可交接。 -> **适用**:所有 UI 新界面 / 界面大改;小改动走 §10 快速通道。 +> **适用**:所有 UI 新界面 / 界面大改;小改动走「小改动快速通道」一节(见文末第十节)。 > **配套文档**:`UI_设计架构.md`(系统如何工作)、`菜单页面设计.md`(菜单/导航行为与数据流设计)、`LVGL模拟器部署与使用说明.md`(模拟器)、`UI_中文字体生成与配置.md`(字体生成)、`UI_自动化测试流程.md`(P2/P3 回归门禁的可执行版本:脚本 + 池水位常量 + 黄金参考更新)。 > **首个实例**:界面 01 主界面(根页),规格 `mockups/01_主界面_说明.md`(本文所有示例数字均取自该试点)。 @@ -24,7 +24,7 @@ flowchart TD P3 -- "Gate: @build 0 错 0 警 + @test PASS + 上机目检通过" --> P4(["P4 归档
→ 变更记录/台账 + 随代码提交(代码+字体+字集+规格+文档)"]) ``` -> 小改动(文案/间距/纯交互/字体重生成)走 §10 快速通道,无需全流程。 +> 小改动(文案/间距/纯交互/字体重生成)走「小改动快速通道」一节(见文末第十节),无需全流程。 ### 1.1 各阶段职责分工 @@ -160,7 +160,7 @@ flowchart TD | 黑/白背景 | `lv_style_set_bg_color` + `lv_style_set_bg_opa` | 1bpp 仅纯黑/纯白 | | 文本 | `lv_label_create` + `lv_label_set_text_static` | 静态字符串(C5) | | 文字对齐 | 标签定位 / `lv_label_set_long_mode` | 默认左对齐(见 §3.6 推论) | -| 选中反色 | `LV_STATE_CHECKED` 样式(bg/文字色互换) | 条目对象**自身**带 CHECKED(防白底白字,架构 §5.5) | +| 选中反色 | `LV_STATE_CHECKED` 样式(bg/文字色互换) | 条目对象**自身**带 CHECKED(防白底白字,`UI_设计架构.md` §5.5) | | 隐藏状态 | `LV_OBJ_FLAG_HIDDEN` | 页面/状态切换 | | 圆角/阴影/透明/渐变 | **禁止**(C8) | 规格里就不该有 | @@ -175,8 +175,8 @@ flowchart TD ### 4.3 翻译纪律 1. **逐参数翻译**:代码中每个 px 值必须能在规格"布局参数表"中找到出处;禁止"看着办" 2. 样式沿用现有 `s_style_flat` 模式(静态样式、一次性初始化、后加优先覆盖主题) -3. 焦点对象恒为**页面容器**;条目不入输入组(架构 §5.4) -4. 容器**永不删除**(indev 释放事件 use-after-free,架构 §5.2);只建/毁框体(行自绘、无对象) +3. 焦点对象恒为**页面容器**;条目不入输入组(`UI_设计架构.md` §5.4) +4. 容器**永不删除**(indev 释放事件 use-after-free,`UI_设计架构.md` §5.2);只建/毁框体(行自绘、无对象) 5. 编码纪律(C6):Edit 工具精确替换;文件保持 UTF-8 BOM 不变 6. 注释按代码规范 V1.0:文件头模块块 + 函数头块(功能/参数/返回/限定/说明) @@ -241,7 +241,7 @@ flowchart TD ### 5.4 Gate(进入 P3) - [ ] 模拟器出图 `shot.png` 与规格参数表逐项一致、与参考图结构一致(§5.2,忽略 5.3) -- [ ] 导航行为符合键位语义表(架构 §3.4) +- [ ] 导航行为符合键位语义表(`UI_设计架构.md` §3.4) - [ ] 自动化回归 PASS:`check_menu_rows.py` + `check_content_pages.py`(`UI_自动化测试流程.md`) --- @@ -307,17 +307,17 @@ flowchart TD | 用了字体子集外的字 | 该字显示空白 | P0 字符集核对 + P1 字体重生成 | | 字体缺字形(如宋体无 ▶) | 该字符显示成方框 □ | `make_font.py` 已内置 `merge_arrow_glyph()` 自动补回;其余缺字形同法(从有该字的字体提取位图合并,§4.4) | | 新 .c 只注册了 Keil(或只改了 CMake) | 单端编译缺函数/静默缺失 | 双端注册(C9) | -| 模拟器主循环调 `SDL_PollEvent` | 键盘全部无响应 | 事件只由 LVGL SDL 驱动内部轮询(架构 §5.7) | -| 选中行"白底白字" | 选中文字不可见 | 历史坑(2026-09-02):子 label 自身 card 样式总命中、继承不到父选中色;现行行自绘(绘制时给色、无样式继承)已根除(架构 §5.5) | -| 按键处理里删除焦点容器 | use-after-free 崩溃 | 容器持久不删(架构 §5.2) | +| 模拟器主循环调 `SDL_PollEvent` | 键盘全部无响应 | 事件只由 LVGL SDL 驱动内部轮询(`UI_设计架构.md` §5.7) | +| 选中行"白底白字" | 选中文字不可见 | 历史坑(2026-09-02):子 label 自身 card 样式总命中、继承不到父选中色;现行行自绘(绘制时给色、无样式继承)已根除(`UI_设计架构.md` §5.5) | +| 按键处理里删除焦点容器 | use-after-free 崩溃 | 容器持久不删(`UI_设计架构.md` §5.2) | | LVGL 池爆 40KB | 对象创建失败/画面残缺 | §4.5 预算 + 按需建毁 | | 极性配反 | 黑屏/白屏/不可读 | 模拟器 mono 主题 `dark_mode` 参数 + MCU 端背景色一致 | | 步进宽接近/超出格宽的标签居中 | 亚像素错位/不对称 | 左对齐 + 字体侧边距(§3.6) | -| 关闭了 `LV_DRAW_SW_COMPLEX`(省代码) | `lv_line` 斜线不绘制(如主界面顶部"浅 V"两腿消失,只剩水平线) | `LV_DRAW_SW_COMPLEX` 两端显式 =1;`LV_DRAW_SW_CIRCLE_CACHE_SIZE` 须非零(默认 4,否则零长数组 C2466)(架构 §关键约束 7) | +| 关闭了 `LV_DRAW_SW_COMPLEX`(省代码) | `lv_line` 斜线不绘制(如主界面顶部"浅 V"两腿消失,只剩水平线) | `LV_DRAW_SW_COMPLEX` 两端显式 =1;`LV_DRAW_SW_CIRCLE_CACHE_SIZE` 须非零(默认 4,否则零长数组 C2466)(`UI_设计架构.md` §关键约束 7) | | 斜线用细 `line_width`(如 2px) | 1bpp 阈值吃掉斜线边缘,腿偏细、比设计稿细 | 斜线加粗(腿 3px),或按段拆 `lv_line` 分粗细(如"浅 V"两腿 3 / 水平线 2) | | 规格引入翻译不出来的效果 | P1 翻译不出来 | 1bpp 禁区(C8),P0 阶段就拦下 | | UI 源文件(UTF-8 BOM)里 `snprintf` 格式串含中文 | 模拟器 cl.exe 报 C4819(代码页 936 误读 UTF-8 字节) | 格式化输出用 `lv_snprintf`(LVGL 自带,双端一致),勿用标准库 `snprintf` | -| 常驻样式用动态 `lv_style_t` + set_prop | LVGL 池永久占用:首次 set_prop 分配 `values_and_props` 且永不释放(x64 实测 88 B/个) | 常驻样式一律常量样式:`LV_STYLE_CONST_INIT(var, props[])` + `lv_style_const_prop_t[]`(属性存 Flash、零池分配,见架构 §5.6) | +| 常驻样式用动态 `lv_style_t` + set_prop | LVGL 池永久占用:首次 set_prop 分配 `values_and_props` 且永不释放(x64 实测 88 B/个) | 常驻样式一律常量样式:`LV_STYLE_CONST_INIT(var, props[])` + `lv_style_const_prop_t[]`(属性存 Flash、零池分配,见`UI_设计架构.md` §5.6) | --- diff --git a/docs/UI设计/UI_自动化测试流程.md b/docs/UI设计/UI_自动化测试流程.md index e1f8d4ba..ab1a2b7a 100644 --- a/docs/UI设计/UI_自动化测试流程.md +++ b/docs/UI设计/UI_自动化测试流程.md @@ -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 | diff --git a/docs/UI设计/UI_设计架构.md b/docs/UI设计/UI_设计架构.md index b5bb1b5e..0795abe7 100644 --- a/docs/UI设计/UI_设计架构.md +++ b/docs/UI设计/UI_设计架构.md @@ -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 ` 无头截图验证 | | `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 | @@ -43,7 +43,7 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓 ### 1.4 硬约束(改 UI 必须遵守) -1. **RAM 紧张**:新增页面/控件/缓冲前必须核算 LVGL 池(40KB)与任务栈余量(见 `待处理内容.md`) +1. **RAM 紧张**:新增页面/控件/缓冲前必须核算 LVGL 池(40KB)与任务栈余量(见 `docs/任务规划/基线与待确认.md` §4 内存预算) 2. **新增汉字必须重新生成字体**(`tools/font_gen/` 两步),否则显示空白 3. 文本一律用静态字符串(`lv_label_set_text_static`),不 strdup 4. UI 相关文件为 **UTF-8 带 BOM**(`Drivers/BSP/UI/*`、`ui_task.c`、`Lib/lv_conf.h`),其余源码无 BOM;禁止 PowerShell 整文件重写(编码铁律,见 AGENTS.md) @@ -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。 --- diff --git a/docs/images/引用关系图.dot b/docs/images/引用关系图.dot index 5440d533..792167f0 100644 --- a/docs/images/引用关系图.dot +++ b/docs/images/引用关系图.dot @@ -2,6 +2,18 @@ digraph refgraph_full { graph [rankdir=LR, bgcolor="white", labelloc=t, label="STM32F4-Base 项目文件引用关系图(项目代码视图)", fontname="Microsoft YaHei", fontsize=18]; node [fontname="Microsoft YaHei", fontsize=10, shape=box, style="rounded,filled"]; edge [fontname="Microsoft YaHei", fontsize=8]; + "App/algo/adc_health.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/algo/adc_health.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/algo/adc_input.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/algo/adc_input.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/algo/phasor.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/algo/phasor.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/algo/prot_elm.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/algo/prot_elm.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/algo/prot_set.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/algo/prot_set.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/algo/sample_ring.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/algo/sample_ring.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/app/app_cfg.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/app/app_main.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/app/app_main.h" [fillcolor="#dbeafe", color="#1d4ed8"]; @@ -16,20 +28,34 @@ digraph refgraph_full { "App/log/console_log.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/log/sys_log.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/log/sys_log_mcu.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/protocol/iec104/iec104_asdu.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/protocol/iec104/iec104_asdu.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/protocol/iec104/iec104_link.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/protocol/iec104/iec104_link.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/protocol/iec104/iec104_pts.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/protocol/iec104/iec104_pts.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/sync/time_sync.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/sync/time_sync.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/task/adc_task.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/task/adc_task.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/task/default_task.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/task/default_task.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/task/iec104_serv.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/task/iec104_serv.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/task/net_task.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/task/net_task.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/task/protect_task.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/task/protect_task.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/task/rs485_task.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/task/rs485_task.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/task/ui_task.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/task/ui_task.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/util/sha256.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/util/sha256.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/util/sys_clock.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/util/sys_clock.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/util/task_prof.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/util/task_prof.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/util/task_watchdog.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/util/task_watchdog.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "Drivers/BSP/CH395F/ch395f.c" [fillcolor="#dbeafe", color="#1d4ed8"]; @@ -39,12 +65,15 @@ digraph refgraph_full { "Drivers/BSP/GD5F2GQ5UE/nand_ftl.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "Drivers/BSP/GD5F2GQ5UE/nand_ftl.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "Drivers/BSP/NET/lftpd/lftpd.h" [fillcolor="#f3f4f6", color="#9ca3af"]; + "Drivers/BSP/NET/lftpd/lftpd_auth.c" [fillcolor="#f3f4f6", color="#9ca3af"]; "Drivers/BSP/NET/net_config.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "Drivers/BSP/NET/net_select.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "Drivers/BSP/NET/net_select.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "Drivers/BSP/NET/net_socket.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "Drivers/BSP/NET/net_socket.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "Drivers/BSP/NET/net_types.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "Drivers/BSP/NETCFG/ftp_cfg.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "Drivers/BSP/NETCFG/ftp_cfg.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "Drivers/BSP/NETCFG/net_cfg.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "Drivers/BSP/NETCFG/net_cfg.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "Drivers/BSP/RS485/rs485.c" [fillcolor="#dbeafe", color="#1d4ed8"]; @@ -114,6 +143,7 @@ digraph refgraph_full { "Inc/spi.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "Inc/stm32f4xx_hal_conf.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "Inc/stm32f4xx_it.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "Inc/tim.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "Inc/usart.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "Lib/FatFs/diskio.h" [fillcolor="#f3f4f6", color="#9ca3af"]; "Lib/FatFs/ff.h" [fillcolor="#f3f4f6", color="#9ca3af"]; @@ -136,6 +166,7 @@ digraph refgraph_full { "Src/stm32f4xx_hal_timebase_tim.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "Src/stm32f4xx_it.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "Src/system_stm32f4xx.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "Src/tim.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "Src/usart.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "simulator/SDL2/SDL2-2.32.10/include/SDL.h" [fillcolor="#f3f4f6", color="#9ca3af"]; "simulator/dbg_log.h" [fillcolor="#dbeafe", color="#1d4ed8"]; @@ -148,6 +179,8 @@ digraph refgraph_full { "simulator/sim/sim_port.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "simulator/sim/sim_syslog.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "simulator/sim/ui_net_sink_backend_sim.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/board/ch395f_reuse_bench.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/board/ch395f_reuse_bench.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/board/ch395f_test.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/board/ch395f_test_task.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/board/ch395f_test_task.h" [fillcolor="#dbeafe", color="#1d4ed8"]; @@ -164,18 +197,65 @@ digraph refgraph_full { "test/board/test_common.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/board/test_config.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/board/test_config.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/adc_health/adc_health_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/adc_input/adc_input_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/adc_input/stub/adc.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/adc_input/stub/tpafe5160.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/cfg_file/cfg_file_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/cfg_file/stub/dbg_log.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/ftp_auth/ftp_auth_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/iec104/iec104_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/phasor/phasor_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/prot_elm/prot_elm_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/prot_set/prot_set_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/sha256/sha256_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/sys_clock/stub/FreeRTOS.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/sys_clock/stub/dbg_log.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/sys_clock/stub/main.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/sys_clock/stub/task.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/sys_clock/sys_clock_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/time_sync/stub/FreeRTOS.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/time_sync/stub/cmsis_os2.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/time_sync/stub/dbg_log.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/time_sync/stub/net_socket.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/time_sync/stub/sys_clock.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/time_sync/stub/sys_events.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/time_sync/stub/task.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/time_sync/time_sync_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/vol_lock/stub/cmsis_os.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/vol_lock/vol_lock_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/watchdog/stub/dbg_log.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/watchdog/stub/iwdg.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/watchdog/stub/main.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/watchdog/watchdog_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "外部/系统头文件" [shape=ellipse, fillcolor="#fef3c7", color="#d97706", fontsize=12]; + "App/algo/adc_health.c" -> "App/algo/adc_health.h" [color="#2563eb", penwidth=1.0]; + "App/algo/adc_health.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/algo/adc_input.c" -> "App/algo/adc_input.h" [color="#2563eb", penwidth=1.0]; + "App/algo/adc_input.c" -> "Drivers/BSP/TPAFE5160/tpafe5160.h" [color="#2563eb", penwidth=1.0]; + "App/algo/adc_input.c" -> "Inc/adc.h" [color="#2563eb", penwidth=1.0]; + "App/algo/adc_input.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/algo/adc_input.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/algo/phasor.c" -> "App/algo/phasor.h" [color="#2563eb", penwidth=1.0]; + "App/algo/phasor.c" -> "App/algo/sample_ring.h" [color="#2563eb", penwidth=1.0]; + "App/algo/phasor.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/algo/phasor.h" -> "App/algo/adc_input.h" [color="#2563eb", penwidth=1.0]; + "App/algo/phasor.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/algo/prot_elm.c" -> "App/algo/phasor.h" [color="#2563eb", penwidth=1.0]; + "App/algo/prot_elm.c" -> "App/algo/prot_elm.h" [color="#2563eb", penwidth=1.0]; + "App/algo/prot_elm.c" -> "App/algo/prot_set.h" [color="#2563eb", penwidth=1.0]; + "App/algo/prot_elm.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/algo/prot_elm.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/algo/prot_set.c" -> "App/algo/prot_set.h" [color="#2563eb", penwidth=1.0]; + "App/algo/prot_set.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/algo/sample_ring.c" -> "App/algo/sample_ring.h" [color="#2563eb", penwidth=1.0]; + "App/algo/sample_ring.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/algo/sample_ring.h" -> "App/algo/adc_input.h" [color="#2563eb", penwidth=1.0]; + "App/algo/sample_ring.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/app/app_main.c" -> "App/algo/adc_input.h" [color="#2563eb", penwidth=1.0]; + "App/app/app_main.c" -> "App/algo/phasor.h" [color="#2563eb", penwidth=1.0]; + "App/app/app_main.c" -> "App/algo/prot_elm.h" [color="#2563eb", penwidth=1.0]; + "App/app/app_main.c" -> "App/algo/prot_set.h" [color="#2563eb", penwidth=1.0]; "App/app/app_main.c" -> "App/app/app_cfg.h" [color="#2563eb", penwidth=1.0]; "App/app/app_main.c" -> "App/app/app_main.h" [color="#2563eb", penwidth=1.0]; "App/app/app_main.c" -> "App/log/sys_log.h" [color="#2563eb", penwidth=1.0]; @@ -188,7 +268,9 @@ digraph refgraph_full { "App/app/app_main.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; "App/app/app_main.c" -> "Inc/main.h" [color="#2563eb", penwidth=1.0]; "App/app/app_main.c" -> "Inc/stm32f4xx_it.h" [color="#2563eb", penwidth=1.0]; + "App/app/app_main.c" -> "Inc/tim.h" [color="#2563eb", penwidth=1.0]; "App/app/app_main.c" -> "Inc/usart.h" [color="#2563eb", penwidth=1.0]; + "App/app/app_main.c" -> "test/board/test_config.h" [color="#2563eb", penwidth=1.0]; "App/app/app_main.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/app/sys_events.h" -> "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h" [color="#94a3b8", penwidth=0.7]; "App/cfg/cfg_file.c" -> "App/cfg/cfg_file.h" [color="#2563eb", penwidth=1.0]; @@ -222,6 +304,22 @@ digraph refgraph_full { "App/log/sys_log_mcu.c" -> "Lib/FatFs/ff.h" [color="#94a3b8", penwidth=0.7]; "App/log/sys_log_mcu.c" -> "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h" [color="#94a3b8", penwidth=0.7]; "App/log/sys_log_mcu.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/protocol/iec104/iec104_asdu.c" -> "App/protocol/iec104/iec104_asdu.h" [color="#2563eb", penwidth=1.0]; + "App/protocol/iec104/iec104_asdu.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/protocol/iec104/iec104_asdu.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/protocol/iec104/iec104_link.c" -> "App/protocol/iec104/iec104_asdu.h" [color="#2563eb", penwidth=1.0]; + "App/protocol/iec104/iec104_link.c" -> "App/protocol/iec104/iec104_link.h" [color="#2563eb", penwidth=1.0]; + "App/protocol/iec104/iec104_link.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/protocol/iec104/iec104_link.h" -> "App/protocol/iec104/iec104_pts.h" [color="#2563eb", penwidth=1.0]; + "App/protocol/iec104/iec104_link.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/protocol/iec104/iec104_pts.c" -> "App/algo/adc_input.h" [color="#2563eb", penwidth=1.0]; + "App/protocol/iec104/iec104_pts.c" -> "App/algo/phasor.h" [color="#2563eb", penwidth=1.0]; + "App/protocol/iec104/iec104_pts.c" -> "App/algo/prot_elm.h" [color="#2563eb", penwidth=1.0]; + "App/protocol/iec104/iec104_pts.c" -> "App/algo/prot_set.h" [color="#2563eb", penwidth=1.0]; + "App/protocol/iec104/iec104_pts.c" -> "App/protocol/iec104/iec104_asdu.h" [color="#2563eb", penwidth=1.0]; + "App/protocol/iec104/iec104_pts.c" -> "App/protocol/iec104/iec104_pts.h" [color="#2563eb", penwidth=1.0]; + "App/protocol/iec104/iec104_pts.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/protocol/iec104/iec104_pts.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/sync/time_sync.c" -> "App/app/sys_events.h" [color="#2563eb", penwidth=1.0]; "App/sync/time_sync.c" -> "App/sync/time_sync.h" [color="#2563eb", penwidth=1.0]; "App/sync/time_sync.c" -> "App/util/sys_clock.h" [color="#2563eb", penwidth=1.0]; @@ -230,7 +328,15 @@ digraph refgraph_full { "App/sync/time_sync.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; "App/sync/time_sync.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/sync/time_sync.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/task/adc_task.c" -> "App/algo/adc_health.h" [color="#2563eb", penwidth=1.0]; + "App/task/adc_task.c" -> "App/algo/adc_input.h" [color="#2563eb", penwidth=1.0]; + "App/task/adc_task.c" -> "App/algo/phasor.h" [color="#2563eb", penwidth=1.0]; + "App/task/adc_task.c" -> "App/algo/sample_ring.h" [color="#2563eb", penwidth=1.0]; + "App/task/adc_task.c" -> "App/log/console_log.h" [color="#2563eb", penwidth=1.0]; + "App/task/adc_task.c" -> "App/log/sys_log.h" [color="#2563eb", penwidth=1.0]; "App/task/adc_task.c" -> "App/task/adc_task.h" [color="#2563eb", penwidth=1.0]; + "App/task/adc_task.c" -> "App/util/task_prof.h" [color="#2563eb", penwidth=1.0]; + "App/task/adc_task.c" -> "App/util/task_watchdog.h" [color="#2563eb", penwidth=1.0]; "App/task/adc_task.c" -> "Drivers/BSP/TPAFE5160/tpafe5160.h" [color="#2563eb", penwidth=1.0]; "App/task/adc_task.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; "App/task/adc_task.c" -> "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h" [color="#94a3b8", penwidth=0.7]; @@ -239,6 +345,7 @@ digraph refgraph_full { "App/task/default_task.c" -> "App/fs/vol_lock.h" [color="#2563eb", penwidth=1.0]; "App/task/default_task.c" -> "App/log/sys_log.h" [color="#2563eb", penwidth=1.0]; "App/task/default_task.c" -> "App/task/default_task.h" [color="#2563eb", penwidth=1.0]; + "App/task/default_task.c" -> "App/util/task_prof.h" [color="#2563eb", penwidth=1.0]; "App/task/default_task.c" -> "App/util/task_watchdog.h" [color="#2563eb", penwidth=1.0]; "App/task/default_task.c" -> "Drivers/BSP/GD5F2GQ5UE/nand_ftl.h" [color="#2563eb", penwidth=1.0]; "App/task/default_task.c" -> "Drivers/BSP/NETCFG/net_cfg.h" [color="#2563eb", penwidth=1.0]; @@ -249,9 +356,20 @@ digraph refgraph_full { "App/task/default_task.c" -> "Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h" [color="#94a3b8", penwidth=0.7]; "App/task/default_task.c" -> "Middlewares/Third_Party/FreeRTOS/Source/include/task.h" [color="#94a3b8", penwidth=0.7]; "App/task/default_task.c" -> "test/board/test_config.h" [color="#2563eb", penwidth=1.0]; + "App/task/iec104_serv.c" -> "App/app/sys_events.h" [color="#2563eb", penwidth=1.0]; + "App/task/iec104_serv.c" -> "App/protocol/iec104/iec104_link.h" [color="#2563eb", penwidth=1.0]; + "App/task/iec104_serv.c" -> "App/protocol/iec104/iec104_pts.h" [color="#2563eb", penwidth=1.0]; + "App/task/iec104_serv.c" -> "App/task/iec104_serv.h" [color="#2563eb", penwidth=1.0]; + "App/task/iec104_serv.c" -> "App/util/sys_clock.h" [color="#2563eb", penwidth=1.0]; + "App/task/iec104_serv.c" -> "App/util/task_watchdog.h" [color="#2563eb", penwidth=1.0]; + "App/task/iec104_serv.c" -> "Drivers/BSP/NET/net_socket.h" [color="#2563eb", penwidth=1.0]; + "App/task/iec104_serv.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; + "App/task/iec104_serv.c" -> "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h" [color="#94a3b8", penwidth=0.7]; + "App/task/iec104_serv.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/task/net_task.c" -> "App/app/sys_events.h" [color="#2563eb", penwidth=1.0]; "App/task/net_task.c" -> "App/log/sys_log.h" [color="#2563eb", penwidth=1.0]; "App/task/net_task.c" -> "App/task/net_task.h" [color="#2563eb", penwidth=1.0]; + "App/task/net_task.c" -> "App/util/task_prof.h" [color="#2563eb", penwidth=1.0]; "App/task/net_task.c" -> "App/util/task_watchdog.h" [color="#2563eb", penwidth=1.0]; "App/task/net_task.c" -> "Drivers/BSP/CH395F/ch395f.h" [color="#2563eb", penwidth=1.0]; "App/task/net_task.c" -> "Drivers/BSP/NET/net_select.h" [color="#2563eb", penwidth=1.0]; @@ -261,6 +379,13 @@ digraph refgraph_full { "App/task/net_task.c" -> "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h" [color="#94a3b8", penwidth=0.7]; "App/task/net_task.c" -> "test/board/test_config.h" [color="#2563eb", penwidth=1.0]; "App/task/net_task.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/task/protect_task.c" -> "App/algo/adc_health.h" [color="#2563eb", penwidth=1.0]; + "App/task/protect_task.c" -> "App/algo/prot_elm.h" [color="#2563eb", penwidth=1.0]; + "App/task/protect_task.c" -> "App/algo/prot_set.h" [color="#2563eb", penwidth=1.0]; + "App/task/protect_task.c" -> "App/task/protect_task.h" [color="#2563eb", penwidth=1.0]; + "App/task/protect_task.c" -> "App/util/task_watchdog.h" [color="#2563eb", penwidth=1.0]; + "App/task/protect_task.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; + "App/task/protect_task.c" -> "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h" [color="#94a3b8", penwidth=0.7]; "App/task/rs485_task.c" -> "App/task/rs485_task.h" [color="#2563eb", penwidth=1.0]; "App/task/rs485_task.c" -> "Drivers/BSP/RS485/rs485.h" [color="#2563eb", penwidth=1.0]; "App/task/rs485_task.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; @@ -268,6 +393,7 @@ digraph refgraph_full { "App/task/ui_task.c" -> "App/app/sys_events.h" [color="#2563eb", penwidth=1.0]; "App/task/ui_task.c" -> "App/task/ui_task.h" [color="#2563eb", penwidth=1.0]; "App/task/ui_task.c" -> "App/util/sys_clock.h" [color="#2563eb", penwidth=1.0]; + "App/task/ui_task.c" -> "App/util/task_prof.h" [color="#2563eb", penwidth=1.0]; "App/task/ui_task.c" -> "App/util/task_watchdog.h" [color="#2563eb", penwidth=1.0]; "App/task/ui_task.c" -> "Drivers/BSP/UI/ui_menu.h" [color="#2563eb", penwidth=1.0]; "App/task/ui_task.c" -> "Drivers/BSP/UI/ui_net_sink.h" [color="#2563eb", penwidth=1.0]; @@ -278,6 +404,9 @@ digraph refgraph_full { "App/task/ui_task.c" -> "test/board/net_latency_test.h" [color="#2563eb", penwidth=1.0]; "App/task/ui_task.c" -> "test/board/test_config.h" [color="#2563eb", penwidth=1.0]; "App/task/ui_task.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/util/sha256.c" -> "App/util/sha256.h" [color="#2563eb", penwidth=1.0]; + "App/util/sha256.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/util/sha256.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/util/sys_clock.c" -> "App/util/sys_clock.h" [color="#2563eb", penwidth=1.0]; "App/util/sys_clock.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; "App/util/sys_clock.c" -> "Inc/main.h" [color="#2563eb", penwidth=1.0]; @@ -286,6 +415,11 @@ digraph refgraph_full { "App/util/sys_clock.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/util/sys_clock.h" -> "Drivers/BSP/SD2506/sd2506.h" [color="#2563eb", penwidth=1.0]; "App/util/sys_clock.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/util/task_prof.c" -> "App/log/console_log.h" [color="#2563eb", penwidth=1.0]; + "App/util/task_prof.c" -> "App/util/task_prof.h" [color="#2563eb", penwidth=1.0]; + "App/util/task_prof.c" -> "Inc/main.h" [color="#2563eb", penwidth=1.0]; + "App/util/task_prof.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/util/task_prof.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/util/task_watchdog.c" -> "App/log/console_log.h" [color="#2563eb", penwidth=1.0]; "App/util/task_watchdog.c" -> "App/log/sys_log.h" [color="#2563eb", penwidth=1.0]; "App/util/task_watchdog.c" -> "App/util/task_watchdog.h" [color="#2563eb", penwidth=1.0]; @@ -341,7 +475,12 @@ digraph refgraph_full { "Drivers/BSP/NET/net_socket.h" -> "Middlewares/Third_Party/FreeRTOS/Source/include/task.h" [color="#94a3b8", penwidth=0.7]; "Drivers/BSP/NET/net_types.h" -> "Drivers/BSP/NET/net_config.h" [color="#2563eb", penwidth=1.0]; "Drivers/BSP/NET/net_types.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "Drivers/BSP/NETCFG/ftp_cfg.c" -> "App/cfg/cfg_file.h" [color="#2563eb", penwidth=1.0]; + "Drivers/BSP/NETCFG/ftp_cfg.c" -> "Drivers/BSP/NETCFG/ftp_cfg.h" [color="#2563eb", penwidth=1.0]; + "Drivers/BSP/NETCFG/ftp_cfg.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "Drivers/BSP/NETCFG/ftp_cfg.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "Drivers/BSP/NETCFG/net_cfg.c" -> "App/cfg/cfg_file.h" [color="#2563eb", penwidth=1.0]; + "Drivers/BSP/NETCFG/net_cfg.c" -> "Drivers/BSP/NETCFG/ftp_cfg.h" [color="#2563eb", penwidth=1.0]; "Drivers/BSP/NETCFG/net_cfg.c" -> "Drivers/BSP/NETCFG/net_cfg.h" [color="#2563eb", penwidth=1.0]; "Drivers/BSP/NETCFG/net_cfg.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; "Drivers/BSP/NETCFG/net_cfg.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; @@ -474,13 +613,17 @@ digraph refgraph_full { "Inc/stm32f4xx_hal_conf.h" -> "Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_tim.h" [color="#94a3b8", penwidth=0.7]; "Inc/stm32f4xx_hal_conf.h" -> "Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_uart.h" [color="#94a3b8", penwidth=0.7]; "Inc/stm32f4xx_hal_conf.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "Inc/tim.h" -> "Inc/main.h" [color="#2563eb", penwidth=1.0]; "Inc/usart.h" -> "Inc/main.h" [color="#2563eb", penwidth=1.0]; "Src/adc.c" -> "Inc/adc.h" [color="#2563eb", penwidth=1.0]; "Src/dma.c" -> "Inc/dma.h" [color="#2563eb", penwidth=1.0]; "Src/freertos.c" -> "App/app/sys_events.h" [color="#2563eb", penwidth=1.0]; "Src/freertos.c" -> "App/sync/time_sync.h" [color="#2563eb", penwidth=1.0]; + "Src/freertos.c" -> "App/task/adc_task.h" [color="#2563eb", penwidth=1.0]; "Src/freertos.c" -> "App/task/default_task.h" [color="#2563eb", penwidth=1.0]; + "Src/freertos.c" -> "App/task/iec104_serv.h" [color="#2563eb", penwidth=1.0]; "Src/freertos.c" -> "App/task/net_task.h" [color="#2563eb", penwidth=1.0]; + "Src/freertos.c" -> "App/task/protect_task.h" [color="#2563eb", penwidth=1.0]; "Src/freertos.c" -> "App/util/task_watchdog.h" [color="#2563eb", penwidth=1.0]; "Src/freertos.c" -> "Drivers/BSP/CH395F/ch395f.h" [color="#2563eb", penwidth=1.0]; "Src/freertos.c" -> "Drivers/BSP/NET/lftpd/lftpd.h" [color="#94a3b8", penwidth=0.7]; @@ -498,6 +641,7 @@ digraph refgraph_full { "Src/i2c.c" -> "Inc/i2c.h" [color="#2563eb", penwidth=1.0]; "Src/iwdg.c" -> "Inc/iwdg.h" [color="#2563eb", penwidth=1.0]; "Src/main.c" -> "App/app/app_main.h" [color="#2563eb", penwidth=1.0]; + "Src/main.c" -> "App/task/adc_task.h" [color="#2563eb", penwidth=1.0]; "Src/main.c" -> "App/util/sys_clock.h" [color="#2563eb", penwidth=1.0]; "Src/main.c" -> "App/util/task_watchdog.h" [color="#2563eb", penwidth=1.0]; "Src/main.c" -> "Drivers/BSP/CH395F/ch395f.h" [color="#2563eb", penwidth=1.0]; @@ -515,6 +659,7 @@ digraph refgraph_full { "Src/main.c" -> "Inc/iwdg.h" [color="#2563eb", penwidth=1.0]; "Src/main.c" -> "Inc/main.h" [color="#2563eb", penwidth=1.0]; "Src/main.c" -> "Inc/spi.h" [color="#2563eb", penwidth=1.0]; + "Src/main.c" -> "Inc/tim.h" [color="#2563eb", penwidth=1.0]; "Src/main.c" -> "Inc/usart.h" [color="#2563eb", penwidth=1.0]; "Src/main.c" -> "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h" [color="#94a3b8", penwidth=0.7]; "Src/main.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; @@ -526,6 +671,7 @@ digraph refgraph_full { "Src/stm32f4xx_it.c" -> "Inc/stm32f4xx_it.h" [color="#2563eb", penwidth=1.0]; "Src/stm32f4xx_it.c" -> "Inc/usart.h" [color="#2563eb", penwidth=1.0]; "Src/system_stm32f4xx.c" -> "Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h" [color="#94a3b8", penwidth=0.7]; + "Src/tim.c" -> "Inc/tim.h" [color="#2563eb", penwidth=1.0]; "Src/usart.c" -> "Inc/usart.h" [color="#2563eb", penwidth=1.0]; "simulator/dbg_log.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "simulator/main.c" -> "App/cfg/cfg_file.h" [color="#2563eb", penwidth=1.0]; @@ -554,6 +700,10 @@ digraph refgraph_full { "simulator/sim/ui_net_sink_backend_sim.c" -> "Drivers/BSP/UI/ui_port.h" [color="#2563eb", penwidth=1.0]; "simulator/sim/ui_net_sink_backend_sim.c" -> "Lib/lvgl/lvgl.h" [color="#94a3b8", penwidth=0.7]; "simulator/sim/ui_net_sink_backend_sim.c" -> "simulator/sim/sim_port.h" [color="#2563eb", penwidth=1.0]; + "test/board/ch395f_reuse_bench.c" -> "Drivers/BSP/CH395F/ch395f.h" [color="#2563eb", penwidth=1.0]; + "test/board/ch395f_reuse_bench.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; + "test/board/ch395f_reuse_bench.c" -> "Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h" [color="#94a3b8", penwidth=0.7]; + "test/board/ch395f_reuse_bench.c" -> "test/board/ch395f_reuse_bench.h" [color="#2563eb", penwidth=1.0]; "test/board/ch395f_test.h" -> "test/board/test_common.h" [color="#2563eb", penwidth=1.0]; "test/board/ch395f_test.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "test/board/ch395f_test_task.c" -> "App/app/sys_events.h" [color="#2563eb", penwidth=1.0]; @@ -632,12 +782,44 @@ digraph refgraph_full { "test/board/test_config.h" -> "test/board/sd2506_test.h" [color="#2563eb", penwidth=1.0]; "test/board/test_config.h" -> "test/board/storage_test.h" [color="#2563eb", penwidth=1.0]; "test/board/test_config.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/adc_health/adc_health_test.c" -> "App/algo/adc_health.c" [color="#2563eb", penwidth=1.0]; + "test/host/adc_health/adc_health_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/adc_input/adc_input_test.c" -> "App/algo/adc_input.c" [color="#2563eb", penwidth=1.0]; + "test/host/adc_input/adc_input_test.c" -> "App/algo/adc_input.h" [color="#2563eb", penwidth=1.0]; + "test/host/adc_input/adc_input_test.c" -> "Drivers/BSP/TPAFE5160/tpafe5160.h" [color="#2563eb", penwidth=1.0]; + "test/host/adc_input/adc_input_test.c" -> "Inc/adc.h" [color="#2563eb", penwidth=1.0]; + "test/host/adc_input/adc_input_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/adc_input/stub/adc.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/adc_input/stub/tpafe5160.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "test/host/cfg_file/cfg_file_test.c" -> "App/cfg/cfg_file.c" [color="#2563eb", penwidth=1.0]; "test/host/cfg_file/cfg_file_test.c" -> "App/cfg/cfg_file_store.h" [color="#2563eb", penwidth=1.0]; "test/host/cfg_file/cfg_file_test.c" -> "App/log/sys_log.h" [color="#2563eb", penwidth=1.0]; "test/host/cfg_file/cfg_file_test.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; "test/host/cfg_file/cfg_file_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "test/host/cfg_file/stub/dbg_log.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/ftp_auth/ftp_auth_test.c" -> "App/util/sha256.c" [color="#2563eb", penwidth=1.0]; + "test/host/ftp_auth/ftp_auth_test.c" -> "Drivers/BSP/NET/lftpd/lftpd_auth.c" [color="#94a3b8", penwidth=0.7]; + "test/host/ftp_auth/ftp_auth_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/iec104/iec104_test.c" -> "App/algo/phasor.c" [color="#2563eb", penwidth=1.0]; + "test/host/iec104/iec104_test.c" -> "App/algo/prot_elm.c" [color="#2563eb", penwidth=1.0]; + "test/host/iec104/iec104_test.c" -> "App/algo/prot_set.c" [color="#2563eb", penwidth=1.0]; + "test/host/iec104/iec104_test.c" -> "App/algo/sample_ring.c" [color="#2563eb", penwidth=1.0]; + "test/host/iec104/iec104_test.c" -> "App/protocol/iec104/iec104_asdu.c" [color="#2563eb", penwidth=1.0]; + "test/host/iec104/iec104_test.c" -> "App/protocol/iec104/iec104_link.c" [color="#2563eb", penwidth=1.0]; + "test/host/iec104/iec104_test.c" -> "App/protocol/iec104/iec104_pts.c" [color="#2563eb", penwidth=1.0]; + "test/host/iec104/iec104_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/phasor/phasor_test.c" -> "App/algo/phasor.c" [color="#2563eb", penwidth=1.0]; + "test/host/phasor/phasor_test.c" -> "App/algo/sample_ring.c" [color="#2563eb", penwidth=1.0]; + "test/host/phasor/phasor_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/prot_elm/prot_elm_test.c" -> "App/algo/phasor.c" [color="#2563eb", penwidth=1.0]; + "test/host/prot_elm/prot_elm_test.c" -> "App/algo/prot_elm.c" [color="#2563eb", penwidth=1.0]; + "test/host/prot_elm/prot_elm_test.c" -> "App/algo/prot_set.c" [color="#2563eb", penwidth=1.0]; + "test/host/prot_elm/prot_elm_test.c" -> "App/algo/sample_ring.c" [color="#2563eb", penwidth=1.0]; + "test/host/prot_elm/prot_elm_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/prot_set/prot_set_test.c" -> "App/algo/prot_set.c" [color="#2563eb", penwidth=1.0]; + "test/host/prot_set/prot_set_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/sha256/sha256_test.c" -> "App/util/sha256.c" [color="#2563eb", penwidth=1.0]; + "test/host/sha256/sha256_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "test/host/sys_clock/stub/FreeRTOS.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "test/host/sys_clock/stub/dbg_log.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "test/host/sys_clock/stub/main.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; @@ -650,6 +832,27 @@ digraph refgraph_full { "test/host/sys_clock/sys_clock_test.c" -> "Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h" [color="#94a3b8", penwidth=0.7]; "test/host/sys_clock/sys_clock_test.c" -> "Middlewares/Third_Party/FreeRTOS/Source/include/task.h" [color="#94a3b8", penwidth=0.7]; "test/host/sys_clock/sys_clock_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/time_sync/stub/FreeRTOS.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/time_sync/stub/cmsis_os2.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/time_sync/stub/dbg_log.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/time_sync/stub/net_socket.h" -> "test/host/time_sync/stub/FreeRTOS.h" [color="#2563eb", penwidth=1.0]; + "test/host/time_sync/stub/net_socket.h" -> "test/host/time_sync/stub/task.h" [color="#2563eb", penwidth=1.0]; + "test/host/time_sync/stub/net_socket.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/time_sync/stub/sys_clock.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/time_sync/stub/sys_events.h" -> "test/host/time_sync/stub/cmsis_os2.h" [color="#2563eb", penwidth=1.0]; + "test/host/time_sync/stub/task.h" -> "test/host/time_sync/stub/FreeRTOS.h" [color="#2563eb", penwidth=1.0]; + "test/host/time_sync/time_sync_test.c" -> "App/app/sys_events.h" [color="#2563eb", penwidth=1.0]; + "test/host/time_sync/time_sync_test.c" -> "App/sync/time_sync.c" [color="#2563eb", penwidth=1.0]; + "test/host/time_sync/time_sync_test.c" -> "App/util/sys_clock.h" [color="#2563eb", penwidth=1.0]; + "test/host/time_sync/time_sync_test.c" -> "App/util/task_watchdog.h" [color="#2563eb", penwidth=1.0]; + "test/host/time_sync/time_sync_test.c" -> "Drivers/BSP/NET/net_socket.h" [color="#2563eb", penwidth=1.0]; + "test/host/time_sync/time_sync_test.c" -> "Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h" [color="#94a3b8", penwidth=0.7]; + "test/host/time_sync/time_sync_test.c" -> "Middlewares/Third_Party/FreeRTOS/Source/include/task.h" [color="#94a3b8", penwidth=0.7]; + "test/host/time_sync/time_sync_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/vol_lock/stub/cmsis_os.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/vol_lock/vol_lock_test.c" -> "App/fs/vol_lock.c" [color="#2563eb", penwidth=1.0]; + "test/host/vol_lock/vol_lock_test.c" -> "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h" [color="#94a3b8", penwidth=0.7]; + "test/host/vol_lock/vol_lock_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "test/host/watchdog/stub/dbg_log.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "test/host/watchdog/stub/iwdg.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "test/host/watchdog/stub/main.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; diff --git a/docs/images/引用关系图.json b/docs/images/引用关系图.json index 9e36ca10..e5d8a2cf 100644 --- a/docs/images/引用关系图.json +++ b/docs/images/引用关系图.json @@ -1,6 +1,54 @@ { "title": "STM32F4-Base 项目文件引用关系图(项目代码视图)", "nodes": [ + { + "path": "App/algo/adc_health.c", + "class": "fp" + }, + { + "path": "App/algo/adc_health.h", + "class": "fp" + }, + { + "path": "App/algo/adc_input.c", + "class": "fp" + }, + { + "path": "App/algo/adc_input.h", + "class": "fp" + }, + { + "path": "App/algo/phasor.c", + "class": "fp" + }, + { + "path": "App/algo/phasor.h", + "class": "fp" + }, + { + "path": "App/algo/prot_elm.c", + "class": "fp" + }, + { + "path": "App/algo/prot_elm.h", + "class": "fp" + }, + { + "path": "App/algo/prot_set.c", + "class": "fp" + }, + { + "path": "App/algo/prot_set.h", + "class": "fp" + }, + { + "path": "App/algo/sample_ring.c", + "class": "fp" + }, + { + "path": "App/algo/sample_ring.h", + "class": "fp" + }, { "path": "App/app/app_cfg.h", "class": "fp" @@ -57,6 +105,30 @@ "path": "App/log/sys_log_mcu.c", "class": "fp" }, + { + "path": "App/protocol/iec104/iec104_asdu.c", + "class": "fp" + }, + { + "path": "App/protocol/iec104/iec104_asdu.h", + "class": "fp" + }, + { + "path": "App/protocol/iec104/iec104_link.c", + "class": "fp" + }, + { + "path": "App/protocol/iec104/iec104_link.h", + "class": "fp" + }, + { + "path": "App/protocol/iec104/iec104_pts.c", + "class": "fp" + }, + { + "path": "App/protocol/iec104/iec104_pts.h", + "class": "fp" + }, { "path": "App/sync/time_sync.c", "class": "fp" @@ -81,6 +153,14 @@ "path": "App/task/default_task.h", "class": "fp" }, + { + "path": "App/task/iec104_serv.c", + "class": "fp" + }, + { + "path": "App/task/iec104_serv.h", + "class": "fp" + }, { "path": "App/task/net_task.c", "class": "fp" @@ -89,6 +169,14 @@ "path": "App/task/net_task.h", "class": "fp" }, + { + "path": "App/task/protect_task.c", + "class": "fp" + }, + { + "path": "App/task/protect_task.h", + "class": "fp" + }, { "path": "App/task/rs485_task.c", "class": "fp" @@ -105,6 +193,14 @@ "path": "App/task/ui_task.h", "class": "fp" }, + { + "path": "App/util/sha256.c", + "class": "fp" + }, + { + "path": "App/util/sha256.h", + "class": "fp" + }, { "path": "App/util/sys_clock.c", "class": "fp" @@ -113,6 +209,14 @@ "path": "App/util/sys_clock.h", "class": "fp" }, + { + "path": "App/util/task_prof.c", + "class": "fp" + }, + { + "path": "App/util/task_prof.h", + "class": "fp" + }, { "path": "App/util/task_watchdog.c", "class": "fp" @@ -153,6 +257,14 @@ "path": "Drivers/BSP/NET/lftpd/lftpd.h", "class": "tp" }, + { + "path": "Drivers/BSP/NET/lftpd/lftpd_auth.c", + "class": "tp" + }, + { + "path": "Drivers/BSP/NET/lftpd/lftpd_auth.h", + "class": "tp" + }, { "path": "Drivers/BSP/NET/lftpd/lftpd_inet.c", "class": "tp" @@ -161,10 +273,6 @@ "path": "Drivers/BSP/NET/lftpd/lftpd_io.c", "class": "tp" }, - { - "path": "Drivers/BSP/NET/lftpd/lftpd_log.c", - "class": "tp" - }, { "path": "Drivers/BSP/NET/lftpd/lftpd_string.c", "class": "tp" @@ -213,6 +321,14 @@ "path": "Drivers/BSP/NET/net_types.h", "class": "fp" }, + { + "path": "Drivers/BSP/NETCFG/ftp_cfg.c", + "class": "fp" + }, + { + "path": "Drivers/BSP/NETCFG/ftp_cfg.h", + "class": "fp" + }, { "path": "Drivers/BSP/NETCFG/net_cfg.c", "class": "fp" @@ -669,6 +785,10 @@ "path": "Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_ll_system.h", "class": "tp" }, + { + "path": "Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_ll_tim.h", + "class": "tp" + }, { "path": "Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_ll_usart.h", "class": "tp" @@ -805,6 +925,10 @@ "path": "Inc/stm32f4xx_it.h", "class": "fp" }, + { + "path": "Inc/tim.h", + "class": "fp" + }, { "path": "Inc/usart.h", "class": "fp" @@ -4789,6 +4913,10 @@ "path": "Src/system_stm32f4xx.c", "class": "fp" }, + { + "path": "Src/tim.c", + "class": "fp" + }, { "path": "Src/usart.c", "class": "fp" @@ -5537,6 +5665,14 @@ "path": "simulator/sim/ui_net_sink_backend_sim.c", "class": "fp" }, + { + "path": "test/board/ch395f_reuse_bench.c", + "class": "fp" + }, + { + "path": "test/board/ch395f_reuse_bench.h", + "class": "fp" + }, { "path": "test/board/ch395f_test.h", "class": "fp" @@ -5601,6 +5737,22 @@ "path": "test/board/test_config.h", "class": "fp" }, + { + "path": "test/host/adc_health/adc_health_test.c", + "class": "fp" + }, + { + "path": "test/host/adc_input/adc_input_test.c", + "class": "fp" + }, + { + "path": "test/host/adc_input/stub/adc.h", + "class": "fp" + }, + { + "path": "test/host/adc_input/stub/tpafe5160.h", + "class": "fp" + }, { "path": "test/host/cfg_file/cfg_file_test.c", "class": "fp" @@ -5609,6 +5761,30 @@ "path": "test/host/cfg_file/stub/dbg_log.h", "class": "fp" }, + { + "path": "test/host/ftp_auth/ftp_auth_test.c", + "class": "fp" + }, + { + "path": "test/host/iec104/iec104_test.c", + "class": "fp" + }, + { + "path": "test/host/phasor/phasor_test.c", + "class": "fp" + }, + { + "path": "test/host/prot_elm/prot_elm_test.c", + "class": "fp" + }, + { + "path": "test/host/prot_set/prot_set_test.c", + "class": "fp" + }, + { + "path": "test/host/sha256/sha256_test.c", + "class": "fp" + }, { "path": "test/host/sys_clock/stub/FreeRTOS.h", "class": "fp" @@ -5629,6 +5805,46 @@ "path": "test/host/sys_clock/sys_clock_test.c", "class": "fp" }, + { + "path": "test/host/time_sync/stub/FreeRTOS.h", + "class": "fp" + }, + { + "path": "test/host/time_sync/stub/cmsis_os2.h", + "class": "fp" + }, + { + "path": "test/host/time_sync/stub/dbg_log.h", + "class": "fp" + }, + { + "path": "test/host/time_sync/stub/net_socket.h", + "class": "fp" + }, + { + "path": "test/host/time_sync/stub/sys_clock.h", + "class": "fp" + }, + { + "path": "test/host/time_sync/stub/sys_events.h", + "class": "fp" + }, + { + "path": "test/host/time_sync/stub/task.h", + "class": "fp" + }, + { + "path": "test/host/time_sync/time_sync_test.c", + "class": "fp" + }, + { + "path": "test/host/vol_lock/stub/cmsis_os.h", + "class": "fp" + }, + { + "path": "test/host/vol_lock/vol_lock_test.c", + "class": "fp" + }, { "path": "test/host/watchdog/stub/dbg_log.h", "class": "fp" @@ -5651,6 +5867,150 @@ } ], "edges": [ + { + "from": "App/algo/adc_health.c", + "to": "App/algo/adc_health.h", + "kind": "fp-fp", + "include": "adc_health.h" + }, + { + "from": "App/algo/adc_health.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "App/algo/adc_input.c", + "to": "App/algo/adc_input.h", + "kind": "fp-fp", + "include": "adc_input.h" + }, + { + "from": "App/algo/adc_input.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stddef.h" + }, + { + "from": "App/algo/adc_input.c", + "to": "Inc/adc.h", + "kind": "fp-fp", + "include": "adc.h" + }, + { + "from": "App/algo/adc_input.c", + "to": "Drivers/BSP/TPAFE5160/tpafe5160.h", + "kind": "fp-fp", + "include": "tpafe5160.h" + }, + { + "from": "App/algo/adc_input.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "App/algo/phasor.c", + "to": "App/algo/phasor.h", + "kind": "fp-fp", + "include": "phasor.h" + }, + { + "from": "App/algo/phasor.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "math.h" + }, + { + "from": "App/algo/phasor.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stddef.h" + }, + { + "from": "App/algo/phasor.c", + "to": "App/algo/sample_ring.h", + "kind": "fp-fp", + "include": "sample_ring.h" + }, + { + "from": "App/algo/phasor.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "App/algo/phasor.h", + "to": "App/algo/adc_input.h", + "kind": "fp-fp", + "include": "adc_input.h" + }, + { + "from": "App/algo/prot_elm.c", + "to": "App/algo/prot_elm.h", + "kind": "fp-fp", + "include": "prot_elm.h" + }, + { + "from": "App/algo/prot_elm.c", + "to": "App/algo/prot_set.h", + "kind": "fp-fp", + "include": "prot_set.h" + }, + { + "from": "App/algo/prot_elm.c", + "to": "App/algo/phasor.h", + "kind": "fp-fp", + "include": "phasor.h" + }, + { + "from": "App/algo/prot_elm.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stddef.h" + }, + { + "from": "App/algo/prot_elm.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "App/algo/prot_set.c", + "to": "App/algo/prot_set.h", + "kind": "fp-fp", + "include": "prot_set.h" + }, + { + "from": "App/algo/prot_set.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "App/algo/sample_ring.c", + "to": "App/algo/sample_ring.h", + "kind": "fp-fp", + "include": "sample_ring.h" + }, + { + "from": "App/algo/sample_ring.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stddef.h" + }, + { + "from": "App/algo/sample_ring.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "App/algo/sample_ring.h", + "to": "App/algo/adc_input.h", + "kind": "fp-fp", + "include": "adc_input.h" + }, { "from": "App/app/app_main.c", "to": "App/app/app_main.h", @@ -5681,6 +6041,12 @@ "kind": "fp-fp", "include": "usart.h" }, + { + "from": "App/app/app_main.c", + "to": "Inc/tim.h", + "kind": "fp-fp", + "include": "tim.h" + }, { "from": "App/app/app_main.c", "to": "Drivers/BSP/RS485/rs485.h", @@ -5711,6 +6077,30 @@ "kind": "fp-fp", "include": "tpafe5160.h" }, + { + "from": "App/app/app_main.c", + "to": "App/algo/adc_input.h", + "kind": "fp-fp", + "include": "adc_input.h" + }, + { + "from": "App/app/app_main.c", + "to": "App/algo/phasor.h", + "kind": "fp-fp", + "include": "phasor.h" + }, + { + "from": "App/app/app_main.c", + "to": "App/algo/prot_set.h", + "kind": "fp-fp", + "include": "prot_set.h" + }, + { + "from": "App/app/app_main.c", + "to": "App/algo/prot_elm.h", + "kind": "fp-fp", + "include": "prot_elm.h" + }, { "from": "App/app/app_main.c", "to": "App/log/sys_log.h", @@ -5723,6 +6113,12 @@ "kind": "fp-fp", "include": "task_watchdog.h" }, + { + "from": "App/app/app_main.c", + "to": "test/board/test_config.h", + "kind": "fp-fp", + "include": "test_config.h" + }, { "from": "App/app/app_main.c", "to": "Drivers/BSP/dbg_log.h", @@ -5975,6 +6371,102 @@ "kind": "fp-fp", "include": "vol_lock.h" }, + { + "from": "App/protocol/iec104/iec104_asdu.c", + "to": "App/protocol/iec104/iec104_asdu.h", + "kind": "fp-fp", + "include": "iec104_asdu.h" + }, + { + "from": "App/protocol/iec104/iec104_asdu.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "string.h" + }, + { + "from": "App/protocol/iec104/iec104_asdu.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "App/protocol/iec104/iec104_link.c", + "to": "App/protocol/iec104/iec104_link.h", + "kind": "fp-fp", + "include": "iec104_link.h" + }, + { + "from": "App/protocol/iec104/iec104_link.c", + "to": "App/protocol/iec104/iec104_asdu.h", + "kind": "fp-fp", + "include": "iec104_asdu.h" + }, + { + "from": "App/protocol/iec104/iec104_link.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "string.h" + }, + { + "from": "App/protocol/iec104/iec104_link.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "App/protocol/iec104/iec104_link.h", + "to": "App/protocol/iec104/iec104_pts.h", + "kind": "fp-fp", + "include": "iec104_pts.h" + }, + { + "from": "App/protocol/iec104/iec104_pts.c", + "to": "App/protocol/iec104/iec104_pts.h", + "kind": "fp-fp", + "include": "iec104_pts.h" + }, + { + "from": "App/protocol/iec104/iec104_pts.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "string.h" + }, + { + "from": "App/protocol/iec104/iec104_pts.c", + "to": "App/protocol/iec104/iec104_asdu.h", + "kind": "fp-fp", + "include": "iec104_asdu.h" + }, + { + "from": "App/protocol/iec104/iec104_pts.c", + "to": "App/algo/phasor.h", + "kind": "fp-fp", + "include": "phasor.h" + }, + { + "from": "App/protocol/iec104/iec104_pts.c", + "to": "App/algo/prot_elm.h", + "kind": "fp-fp", + "include": "prot_elm.h" + }, + { + "from": "App/protocol/iec104/iec104_pts.c", + "to": "App/algo/prot_set.h", + "kind": "fp-fp", + "include": "prot_set.h" + }, + { + "from": "App/protocol/iec104/iec104_pts.c", + "to": "App/algo/adc_input.h", + "kind": "fp-fp", + "include": "adc_input.h" + }, + { + "from": "App/protocol/iec104/iec104_pts.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, { "from": "App/sync/time_sync.c", "to": "外部/系统头文件", @@ -6035,12 +6527,60 @@ "kind": "fp-tp", "include": "cmsis_os.h" }, + { + "from": "App/task/adc_task.c", + "to": "App/algo/adc_input.h", + "kind": "fp-fp", + "include": "adc_input.h" + }, + { + "from": "App/task/adc_task.c", + "to": "App/algo/sample_ring.h", + "kind": "fp-fp", + "include": "sample_ring.h" + }, + { + "from": "App/task/adc_task.c", + "to": "App/algo/phasor.h", + "kind": "fp-fp", + "include": "phasor.h" + }, + { + "from": "App/task/adc_task.c", + "to": "App/algo/adc_health.h", + "kind": "fp-fp", + "include": "adc_health.h" + }, { "from": "App/task/adc_task.c", "to": "Drivers/BSP/TPAFE5160/tpafe5160.h", "kind": "fp-fp", "include": "tpafe5160.h" }, + { + "from": "App/task/adc_task.c", + "to": "App/util/task_watchdog.h", + "kind": "fp-fp", + "include": "task_watchdog.h" + }, + { + "from": "App/task/adc_task.c", + "to": "App/log/console_log.h", + "kind": "fp-fp", + "include": "console_log.h" + }, + { + "from": "App/task/adc_task.c", + "to": "App/log/sys_log.h", + "kind": "fp-fp", + "include": "sys_log.h" + }, + { + "from": "App/task/adc_task.c", + "to": "App/util/task_prof.h", + "kind": "fp-fp", + "include": "task_prof.h" + }, { "from": "App/task/adc_task.c", "to": "Drivers/BSP/dbg_log.h", @@ -6131,12 +6671,78 @@ "kind": "fp-fp", "include": "task_watchdog.h" }, + { + "from": "App/task/default_task.c", + "to": "App/util/task_prof.h", + "kind": "fp-fp", + "include": "task_prof.h" + }, { "from": "App/task/default_task.c", "to": "App/fs/vol_lock.h", "kind": "fp-fp", "include": "vol_lock.h" }, + { + "from": "App/task/iec104_serv.c", + "to": "App/task/iec104_serv.h", + "kind": "fp-fp", + "include": "iec104_serv.h" + }, + { + "from": "App/task/iec104_serv.c", + "to": "App/protocol/iec104/iec104_link.h", + "kind": "fp-fp", + "include": "iec104_link.h" + }, + { + "from": "App/task/iec104_serv.c", + "to": "App/protocol/iec104/iec104_pts.h", + "kind": "fp-fp", + "include": "iec104_pts.h" + }, + { + "from": "App/task/iec104_serv.c", + "to": "Drivers/BSP/NET/net_socket.h", + "kind": "fp-fp", + "include": "net_socket.h" + }, + { + "from": "App/task/iec104_serv.c", + "to": "App/util/sys_clock.h", + "kind": "fp-fp", + "include": "sys_clock.h" + }, + { + "from": "App/task/iec104_serv.c", + "to": "App/app/sys_events.h", + "kind": "fp-fp", + "include": "sys_events.h" + }, + { + "from": "App/task/iec104_serv.c", + "to": "App/util/task_watchdog.h", + "kind": "fp-fp", + "include": "task_watchdog.h" + }, + { + "from": "App/task/iec104_serv.c", + "to": "Drivers/BSP/dbg_log.h", + "kind": "fp-fp", + "include": "dbg_log.h" + }, + { + "from": "App/task/iec104_serv.c", + "to": "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h", + "kind": "fp-tp", + "include": "cmsis_os2.h" + }, + { + "from": "App/task/iec104_serv.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "string.h" + }, { "from": "App/task/net_task.c", "to": "外部/系统头文件", @@ -6203,12 +6809,60 @@ "kind": "fp-fp", "include": "task_watchdog.h" }, + { + "from": "App/task/net_task.c", + "to": "App/util/task_prof.h", + "kind": "fp-fp", + "include": "task_prof.h" + }, { "from": "App/task/net_task.c", "to": "Drivers/BSP/dbg_log.h", "kind": "fp-fp", "include": "dbg_log.h" }, + { + "from": "App/task/protect_task.c", + "to": "App/task/protect_task.h", + "kind": "fp-fp", + "include": "protect_task.h" + }, + { + "from": "App/task/protect_task.c", + "to": "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h", + "kind": "fp-tp", + "include": "cmsis_os2.h" + }, + { + "from": "App/task/protect_task.c", + "to": "Drivers/BSP/dbg_log.h", + "kind": "fp-fp", + "include": "dbg_log.h" + }, + { + "from": "App/task/protect_task.c", + "to": "App/algo/adc_health.h", + "kind": "fp-fp", + "include": "adc_health.h" + }, + { + "from": "App/task/protect_task.c", + "to": "App/algo/prot_elm.h", + "kind": "fp-fp", + "include": "prot_elm.h" + }, + { + "from": "App/task/protect_task.c", + "to": "App/algo/prot_set.h", + "kind": "fp-fp", + "include": "prot_set.h" + }, + { + "from": "App/task/protect_task.c", + "to": "App/util/task_watchdog.h", + "kind": "fp-fp", + "include": "task_watchdog.h" + }, { "from": "App/task/rs485_task.c", "to": "App/task/rs485_task.h", @@ -6305,12 +6959,42 @@ "kind": "fp-fp", "include": "task_watchdog.h" }, + { + "from": "App/task/ui_task.c", + "to": "App/util/task_prof.h", + "kind": "fp-fp", + "include": "task_prof.h" + }, { "from": "App/task/ui_task.c", "to": "Drivers/BSP/dbg_log.h", "kind": "fp-fp", "include": "dbg_log.h" }, + { + "from": "App/util/sha256.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "string.h" + }, + { + "from": "App/util/sha256.c", + "to": "App/util/sha256.h", + "kind": "fp-fp", + "include": "sha256.h" + }, + { + "from": "App/util/sha256.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stddef.h" + }, + { + "from": "App/util/sha256.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, { "from": "App/util/sys_clock.c", "to": "App/util/sys_clock.h", @@ -6365,6 +7049,36 @@ "kind": "fp-fp", "include": "sd2506.h" }, + { + "from": "App/util/task_prof.c", + "to": "App/util/task_prof.h", + "kind": "fp-fp", + "include": "task_prof.h" + }, + { + "from": "App/util/task_prof.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdio.h" + }, + { + "from": "App/util/task_prof.c", + "to": "Inc/main.h", + "kind": "fp-fp", + "include": "main.h" + }, + { + "from": "App/util/task_prof.c", + "to": "App/log/console_log.h", + "kind": "fp-fp", + "include": "console_log.h" + }, + { + "from": "App/util/task_prof.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, { "from": "App/util/task_watchdog.c", "to": "App/util/task_watchdog.h", @@ -6851,6 +7565,18 @@ "kind": "tp-tp", "include": "net_config.h" }, + { + "from": "Drivers/BSP/NET/lftpd/lftpd.c", + "to": "Drivers/BSP/NET/lftpd/lftpd_auth.h", + "kind": "tp-tp", + "include": "lftpd_auth.h" + }, + { + "from": "Drivers/BSP/NET/lftpd/lftpd.c", + "to": "Drivers/BSP/NETCFG/ftp_cfg.h", + "kind": "tp-tp", + "include": "ftp_cfg.h" + }, { "from": "Drivers/BSP/NET/lftpd/lftpd.c", "to": "Lib/FatFs/ff.h", @@ -6863,6 +7589,36 @@ "kind": "tp-ext", "include": "stdint.h" }, + { + "from": "Drivers/BSP/NET/lftpd/lftpd_auth.c", + "to": "外部/系统头文件", + "kind": "tp-ext", + "include": "stddef.h" + }, + { + "from": "Drivers/BSP/NET/lftpd/lftpd_auth.c", + "to": "外部/系统头文件", + "kind": "tp-ext", + "include": "string.h" + }, + { + "from": "Drivers/BSP/NET/lftpd/lftpd_auth.c", + "to": "Drivers/BSP/NET/lftpd/lftpd_auth.h", + "kind": "tp-tp", + "include": "lftpd_auth.h" + }, + { + "from": "Drivers/BSP/NET/lftpd/lftpd_auth.c", + "to": "App/util/sha256.h", + "kind": "tp-tp", + "include": "sha256.h" + }, + { + "from": "Drivers/BSP/NET/lftpd/lftpd_auth.h", + "to": "外部/系统头文件", + "kind": "tp-ext", + "include": "stdint.h" + }, { "from": "Drivers/BSP/NET/lftpd/lftpd_inet.c", "to": "Drivers/BSP/NET/lftpd/private/lftpd_inet.h", @@ -6947,36 +7703,6 @@ "kind": "tp-tp", "include": "dbg_log.h" }, - { - "from": "Drivers/BSP/NET/lftpd/lftpd_log.c", - "to": "Drivers/BSP/NET/lftpd/private/lftpd_log.h", - "kind": "tp-tp", - "include": "private/lftpd_log.h" - }, - { - "from": "Drivers/BSP/NET/lftpd/lftpd_log.c", - "to": "外部/系统头文件", - "kind": "tp-ext", - "include": "stdio.h" - }, - { - "from": "Drivers/BSP/NET/lftpd/lftpd_log.c", - "to": "外部/系统头文件", - "kind": "tp-ext", - "include": "stdlib.h" - }, - { - "from": "Drivers/BSP/NET/lftpd/lftpd_log.c", - "to": "外部/系统头文件", - "kind": "tp-ext", - "include": "stdarg.h" - }, - { - "from": "Drivers/BSP/NET/lftpd/lftpd_log.c", - "to": "Drivers/BSP/dbg_log.h", - "kind": "tp-tp", - "include": "dbg_log.h" - }, { "from": "Drivers/BSP/NET/lftpd/lftpd_string.c", "to": "外部/系统头文件", @@ -7019,6 +7745,36 @@ "kind": "tp-tp", "include": "dbg_log.h" }, + { + "from": "Drivers/BSP/NETCFG/ftp_cfg.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stddef.h" + }, + { + "from": "Drivers/BSP/NETCFG/ftp_cfg.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "string.h" + }, + { + "from": "Drivers/BSP/NETCFG/ftp_cfg.c", + "to": "Drivers/BSP/NETCFG/ftp_cfg.h", + "kind": "fp-fp", + "include": "ftp_cfg.h" + }, + { + "from": "Drivers/BSP/NETCFG/ftp_cfg.c", + "to": "App/cfg/cfg_file.h", + "kind": "fp-fp", + "include": "cfg_file.h" + }, + { + "from": "Drivers/BSP/NETCFG/ftp_cfg.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, { "from": "Drivers/BSP/NETCFG/net_cfg.c", "to": "外部/系统头文件", @@ -7049,6 +7805,12 @@ "kind": "fp-fp", "include": "cfg_file.h" }, + { + "from": "Drivers/BSP/NETCFG/net_cfg.c", + "to": "Drivers/BSP/NETCFG/ftp_cfg.h", + "kind": "fp-fp", + "include": "ftp_cfg.h" + }, { "from": "Drivers/BSP/NETCFG/net_cfg.c", "to": "Drivers/BSP/dbg_log.h", @@ -8675,6 +9437,12 @@ "kind": "tp-tp", "include": "stm32f4xx.h" }, + { + "from": "Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_ll_tim.h", + "to": "Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h", + "kind": "tp-tp", + "include": "stm32f4xx.h" + }, { "from": "Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_ll_usart.h", "to": "Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h", @@ -9167,6 +9935,12 @@ "kind": "fp-ext", "include": "stm32f4xx_hal_mmc.h" }, + { + "from": "Inc/tim.h", + "to": "Inc/main.h", + "kind": "fp-fp", + "include": "main.h" + }, { "from": "Inc/usart.h", "to": "Inc/main.h", @@ -39011,6 +39785,24 @@ "kind": "fp-fp", "include": "default_task.h" }, + { + "from": "Src/freertos.c", + "to": "App/task/adc_task.h", + "kind": "fp-fp", + "include": "adc_task.h" + }, + { + "from": "Src/freertos.c", + "to": "App/task/iec104_serv.h", + "kind": "fp-fp", + "include": "iec104_serv.h" + }, + { + "from": "Src/freertos.c", + "to": "App/task/protect_task.h", + "kind": "fp-fp", + "include": "protect_task.h" + }, { "from": "Src/gpio.c", "to": "Inc/gpio.h", @@ -39071,6 +39863,12 @@ "kind": "fp-fp", "include": "spi.h" }, + { + "from": "Src/main.c", + "to": "Inc/tim.h", + "kind": "fp-fp", + "include": "tim.h" + }, { "from": "Src/main.c", "to": "Inc/usart.h", @@ -39155,6 +39953,12 @@ "kind": "fp-fp", "include": "task_watchdog.h" }, + { + "from": "Src/main.c", + "to": "App/task/adc_task.h", + "kind": "fp-fp", + "include": "adc_task.h" + }, { "from": "Src/main.c", "to": "Drivers/BSP/dbg_log.h", @@ -39209,12 +40013,42 @@ "kind": "fp-tp", "include": "stm32f4xx.h" }, + { + "from": "Src/tim.c", + "to": "Inc/tim.h", + "kind": "fp-fp", + "include": "tim.h" + }, { "from": "Src/usart.c", "to": "Inc/usart.h", "kind": "fp-fp", "include": "usart.h" }, + { + "from": "test/board/ch395f_reuse_bench.c", + "to": "test/board/ch395f_reuse_bench.h", + "kind": "fp-fp", + "include": "ch395f_reuse_bench.h" + }, + { + "from": "test/board/ch395f_reuse_bench.c", + "to": "Drivers/BSP/CH395F/ch395f.h", + "kind": "fp-fp", + "include": "ch395f.h" + }, + { + "from": "test/board/ch395f_reuse_bench.c", + "to": "Drivers/BSP/dbg_log.h", + "kind": "fp-fp", + "include": "dbg_log.h" + }, + { + "from": "test/board/ch395f_reuse_bench.c", + "to": "Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h", + "kind": "fp-tp", + "include": "stm32f4xx_hal.h" + }, { "from": "test/board/ch395f_test.h", "to": "外部/系统头文件", @@ -39695,6 +40529,78 @@ "kind": "fp-fp", "include": "sd2506_test.h" }, + { + "from": "test/host/adc_health/adc_health_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "test/host/adc_health/adc_health_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdio.h" + }, + { + "from": "test/host/adc_health/adc_health_test.c", + "to": "App/algo/adc_health.c", + "kind": "fp-fp", + "include": "adc_health.c" + }, + { + "from": "test/host/adc_input/adc_input_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stddef.h" + }, + { + "from": "test/host/adc_input/adc_input_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "test/host/adc_input/adc_input_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdio.h" + }, + { + "from": "test/host/adc_input/adc_input_test.c", + "to": "Inc/adc.h", + "kind": "fp-fp", + "include": "adc.h" + }, + { + "from": "test/host/adc_input/adc_input_test.c", + "to": "Drivers/BSP/TPAFE5160/tpafe5160.h", + "kind": "fp-fp", + "include": "tpafe5160.h" + }, + { + "from": "test/host/adc_input/adc_input_test.c", + "to": "App/algo/adc_input.h", + "kind": "fp-fp", + "include": "adc_input.h" + }, + { + "from": "test/host/adc_input/adc_input_test.c", + "to": "App/algo/adc_input.c", + "kind": "fp-fp", + "include": "adc_input.c" + }, + { + "from": "test/host/adc_input/stub/adc.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "test/host/adc_input/stub/tpafe5160.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, { "from": "test/host/cfg_file/cfg_file_test.c", "to": "外部/系统头文件", @@ -39749,6 +40655,192 @@ "kind": "fp-ext", "include": "stdio.h" }, + { + "from": "test/host/ftp_auth/ftp_auth_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdio.h" + }, + { + "from": "test/host/ftp_auth/ftp_auth_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "string.h" + }, + { + "from": "test/host/ftp_auth/ftp_auth_test.c", + "to": "App/util/sha256.c", + "kind": "fp-fp", + "include": "sha256.c" + }, + { + "from": "test/host/ftp_auth/ftp_auth_test.c", + "to": "Drivers/BSP/NET/lftpd/lftpd_auth.c", + "kind": "fp-tp", + "include": "lftpd_auth.c" + }, + { + "from": "test/host/iec104/iec104_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "test/host/iec104/iec104_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdio.h" + }, + { + "from": "test/host/iec104/iec104_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "string.h" + }, + { + "from": "test/host/iec104/iec104_test.c", + "to": "App/protocol/iec104/iec104_asdu.c", + "kind": "fp-fp", + "include": "iec104_asdu.c" + }, + { + "from": "test/host/iec104/iec104_test.c", + "to": "App/algo/prot_set.c", + "kind": "fp-fp", + "include": "prot_set.c" + }, + { + "from": "test/host/iec104/iec104_test.c", + "to": "App/algo/sample_ring.c", + "kind": "fp-fp", + "include": "sample_ring.c" + }, + { + "from": "test/host/iec104/iec104_test.c", + "to": "App/algo/phasor.c", + "kind": "fp-fp", + "include": "phasor.c" + }, + { + "from": "test/host/iec104/iec104_test.c", + "to": "App/algo/prot_elm.c", + "kind": "fp-fp", + "include": "prot_elm.c" + }, + { + "from": "test/host/iec104/iec104_test.c", + "to": "App/protocol/iec104/iec104_pts.c", + "kind": "fp-fp", + "include": "iec104_pts.c" + }, + { + "from": "test/host/iec104/iec104_test.c", + "to": "App/protocol/iec104/iec104_link.c", + "kind": "fp-fp", + "include": "iec104_link.c" + }, + { + "from": "test/host/phasor/phasor_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "math.h" + }, + { + "from": "test/host/phasor/phasor_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "test/host/phasor/phasor_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdio.h" + }, + { + "from": "test/host/phasor/phasor_test.c", + "to": "App/algo/sample_ring.c", + "kind": "fp-fp", + "include": "sample_ring.c" + }, + { + "from": "test/host/phasor/phasor_test.c", + "to": "App/algo/phasor.c", + "kind": "fp-fp", + "include": "phasor.c" + }, + { + "from": "test/host/prot_elm/prot_elm_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "test/host/prot_elm/prot_elm_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdio.h" + }, + { + "from": "test/host/prot_elm/prot_elm_test.c", + "to": "App/algo/prot_set.c", + "kind": "fp-fp", + "include": "prot_set.c" + }, + { + "from": "test/host/prot_elm/prot_elm_test.c", + "to": "App/algo/sample_ring.c", + "kind": "fp-fp", + "include": "sample_ring.c" + }, + { + "from": "test/host/prot_elm/prot_elm_test.c", + "to": "App/algo/phasor.c", + "kind": "fp-fp", + "include": "phasor.c" + }, + { + "from": "test/host/prot_elm/prot_elm_test.c", + "to": "App/algo/prot_elm.c", + "kind": "fp-fp", + "include": "prot_elm.c" + }, + { + "from": "test/host/prot_set/prot_set_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "test/host/prot_set/prot_set_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdio.h" + }, + { + "from": "test/host/prot_set/prot_set_test.c", + "to": "App/algo/prot_set.c", + "kind": "fp-fp", + "include": "prot_set.c" + }, + { + "from": "test/host/sha256/sha256_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdio.h" + }, + { + "from": "test/host/sha256/sha256_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "string.h" + }, + { + "from": "test/host/sha256/sha256_test.c", + "to": "App/util/sha256.c", + "kind": "fp-fp", + "include": "sha256.c" + }, { "from": "test/host/sys_clock/sys_clock_test.c", "to": "外部/系统头文件", @@ -39833,6 +40925,162 @@ "kind": "fp-fp", "include": "FreeRTOS.h" }, + { + "from": "test/host/time_sync/time_sync_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "setjmp.h" + }, + { + "from": "test/host/time_sync/time_sync_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "test/host/time_sync/time_sync_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdio.h" + }, + { + "from": "test/host/time_sync/time_sync_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "string.h" + }, + { + "from": "test/host/time_sync/time_sync_test.c", + "to": "Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h", + "kind": "fp-tp", + "include": "FreeRTOS.h" + }, + { + "from": "test/host/time_sync/time_sync_test.c", + "to": "Middlewares/Third_Party/FreeRTOS/Source/include/task.h", + "kind": "fp-tp", + "include": "task.h" + }, + { + "from": "test/host/time_sync/time_sync_test.c", + "to": "Drivers/BSP/NET/net_socket.h", + "kind": "fp-fp", + "include": "net_socket.h" + }, + { + "from": "test/host/time_sync/time_sync_test.c", + "to": "App/util/sys_clock.h", + "kind": "fp-fp", + "include": "sys_clock.h" + }, + { + "from": "test/host/time_sync/time_sync_test.c", + "to": "App/app/sys_events.h", + "kind": "fp-fp", + "include": "sys_events.h" + }, + { + "from": "test/host/time_sync/time_sync_test.c", + "to": "App/util/task_watchdog.h", + "kind": "fp-fp", + "include": "task_watchdog.h" + }, + { + "from": "test/host/time_sync/time_sync_test.c", + "to": "App/sync/time_sync.c", + "kind": "fp-fp", + "include": "time_sync.c" + }, + { + "from": "test/host/time_sync/stub/FreeRTOS.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "test/host/time_sync/stub/cmsis_os2.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "test/host/time_sync/stub/dbg_log.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdio.h" + }, + { + "from": "test/host/time_sync/stub/net_socket.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "test/host/time_sync/stub/net_socket.h", + "to": "test/host/time_sync/stub/FreeRTOS.h", + "kind": "fp-fp", + "include": "FreeRTOS.h" + }, + { + "from": "test/host/time_sync/stub/net_socket.h", + "to": "test/host/time_sync/stub/task.h", + "kind": "fp-fp", + "include": "task.h" + }, + { + "from": "test/host/time_sync/stub/sys_clock.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "test/host/time_sync/stub/sys_events.h", + "to": "test/host/time_sync/stub/cmsis_os2.h", + "kind": "fp-fp", + "include": "cmsis_os2.h" + }, + { + "from": "test/host/time_sync/stub/task.h", + "to": "test/host/time_sync/stub/FreeRTOS.h", + "kind": "fp-fp", + "include": "FreeRTOS.h" + }, + { + "from": "test/host/vol_lock/vol_lock_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stddef.h" + }, + { + "from": "test/host/vol_lock/vol_lock_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "test/host/vol_lock/vol_lock_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdio.h" + }, + { + "from": "test/host/vol_lock/vol_lock_test.c", + "to": "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h", + "kind": "fp-tp", + "include": "cmsis_os.h" + }, + { + "from": "test/host/vol_lock/vol_lock_test.c", + "to": "App/fs/vol_lock.c", + "kind": "fp-fp", + "include": "vol_lock.c" + }, + { + "from": "test/host/vol_lock/stub/cmsis_os.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, { "from": "test/host/watchdog/watchdog_test.c", "to": "外部/系统头文件", @@ -39889,6 +41137,37 @@ } ], "unresolved": { + "App/algo/adc_health.h": [ + "stdint.h" + ], + "App/algo/adc_input.c": [ + "stddef.h" + ], + "App/algo/adc_input.h": [ + "stdint.h" + ], + "App/algo/phasor.c": [ + "math.h", + "stddef.h" + ], + "App/algo/phasor.h": [ + "stdint.h" + ], + "App/algo/prot_elm.c": [ + "stddef.h" + ], + "App/algo/prot_elm.h": [ + "stdint.h" + ], + "App/algo/prot_set.h": [ + "stdint.h" + ], + "App/algo/sample_ring.c": [ + "stddef.h" + ], + "App/algo/sample_ring.h": [ + "stdint.h" + ], "App/app/app_main.h": [ "stdint.h" ], @@ -39927,18 +41206,46 @@ "stdio.h", "string.h" ], + "App/protocol/iec104/iec104_asdu.c": [ + "string.h" + ], + "App/protocol/iec104/iec104_asdu.h": [ + "stdint.h" + ], + "App/protocol/iec104/iec104_link.c": [ + "string.h" + ], + "App/protocol/iec104/iec104_link.h": [ + "stdint.h" + ], + "App/protocol/iec104/iec104_pts.c": [ + "string.h" + ], + "App/protocol/iec104/iec104_pts.h": [ + "stdint.h" + ], "App/sync/time_sync.c": [ "string.h" ], "App/sync/time_sync.h": [ "stdint.h" ], + "App/task/iec104_serv.c": [ + "string.h" + ], "App/task/net_task.c": [ "string.h" ], "App/task/ui_task.c": [ "string.h" ], + "App/util/sha256.c": [ + "string.h" + ], + "App/util/sha256.h": [ + "stddef.h", + "stdint.h" + ], "App/util/sys_clock.c": [ "stdio.h", "string.h" @@ -39946,6 +41253,12 @@ "App/util/sys_clock.h": [ "stdint.h" ], + "App/util/task_prof.c": [ + "stdio.h" + ], + "App/util/task_prof.h": [ + "stdint.h" + ], "App/util/task_watchdog.c": [ "string.h" ], @@ -39980,6 +41293,13 @@ "Drivers/BSP/NET/lftpd/lftpd.h": [ "stdint.h" ], + "Drivers/BSP/NET/lftpd/lftpd_auth.c": [ + "stddef.h", + "string.h" + ], + "Drivers/BSP/NET/lftpd/lftpd_auth.h": [ + "stdint.h" + ], "Drivers/BSP/NET/lftpd/lftpd_inet.c": [ "string.h" ], @@ -39989,11 +41309,6 @@ "stdlib.h", "string.h" ], - "Drivers/BSP/NET/lftpd/lftpd_log.c": [ - "stdarg.h", - "stdio.h", - "stdlib.h" - ], "Drivers/BSP/NET/lftpd/lftpd_string.c": [ "ctype.h", "string.h" @@ -40017,6 +41332,13 @@ "Drivers/BSP/NET/net_types.h": [ "stdint.h" ], + "Drivers/BSP/NETCFG/ftp_cfg.c": [ + "stddef.h", + "string.h" + ], + "Drivers/BSP/NETCFG/ftp_cfg.h": [ + "stdint.h" + ], "Drivers/BSP/NETCFG/net_cfg.c": [ "stdbool.h", "stdio.h", @@ -41617,6 +42939,21 @@ "test/board/test_config.h": [ "stdint.h" ], + "test/host/adc_health/adc_health_test.c": [ + "stdint.h", + "stdio.h" + ], + "test/host/adc_input/adc_input_test.c": [ + "stddef.h", + "stdint.h", + "stdio.h" + ], + "test/host/adc_input/stub/adc.h": [ + "stdint.h" + ], + "test/host/adc_input/stub/tpafe5160.h": [ + "stdint.h" + ], "test/host/cfg_file/cfg_file_test.c": [ "stdint.h", "stdio.h", @@ -41626,6 +42963,32 @@ "test/host/cfg_file/stub/dbg_log.h": [ "stdio.h" ], + "test/host/ftp_auth/ftp_auth_test.c": [ + "stdio.h", + "string.h" + ], + "test/host/iec104/iec104_test.c": [ + "stdint.h", + "stdio.h", + "string.h" + ], + "test/host/phasor/phasor_test.c": [ + "math.h", + "stdint.h", + "stdio.h" + ], + "test/host/prot_elm/prot_elm_test.c": [ + "stdint.h", + "stdio.h" + ], + "test/host/prot_set/prot_set_test.c": [ + "stdint.h", + "stdio.h" + ], + "test/host/sha256/sha256_test.c": [ + "stdio.h", + "string.h" + ], "test/host/sys_clock/stub/FreeRTOS.h": [ "stdint.h" ], @@ -41640,6 +43003,35 @@ "stdio.h", "string.h" ], + "test/host/time_sync/stub/FreeRTOS.h": [ + "stdint.h" + ], + "test/host/time_sync/stub/cmsis_os2.h": [ + "stdint.h" + ], + "test/host/time_sync/stub/dbg_log.h": [ + "stdio.h" + ], + "test/host/time_sync/stub/net_socket.h": [ + "stdint.h" + ], + "test/host/time_sync/stub/sys_clock.h": [ + "stdint.h" + ], + "test/host/time_sync/time_sync_test.c": [ + "setjmp.h", + "stdint.h", + "stdio.h", + "string.h" + ], + "test/host/vol_lock/stub/cmsis_os.h": [ + "stdint.h" + ], + "test/host/vol_lock/vol_lock_test.c": [ + "stddef.h", + "stdint.h", + "stdio.h" + ], "test/host/watchdog/stub/dbg_log.h": [ "stdio.h" ], @@ -41655,12 +43047,12 @@ ] }, "stats": { - "files_indexed": 1425, - "includes_parsed": 5706, - "resolved": 4637, - "unresolved": 1069, - "nodes_fp": 147, - "nodes_tp": 1264, + "files_indexed": 1479, + "includes_parsed": 5878, + "resolved": 4749, + "unresolved": 1129, + "nodes_fp": 199, + "nodes_tp": 1266, "ext_names": 351 }, "include_dirs": [ @@ -41680,11 +43072,13 @@ "Drivers/BSP/NETCFG", "App", "App/app", + "App/algo", "App/cfg", "App/fs", "App/log", "App/sync", "App/task", + "App/protocol/iec104", "App/util", "Middlewares/Third_Party/FreeRTOS/Source/include", "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2", diff --git a/docs/images/引用关系图.svg b/docs/images/引用关系图.svg index ab0679a5..99b87f48 100644 --- a/docs/images/引用关系图.svg +++ b/docs/images/引用关系图.svg @@ -4,3947 +4,5165 @@ - - + + refgraph_full - -STM32F4-Base 项目文件引用关系图(项目代码视图) - + +STM32F4-Base 项目文件引用关系图(项目代码视图) + -App/app/app_cfg.h - -App/app/app_cfg.h +App/algo/adc_health.c + +App/algo/adc_health.c - + -App/app/app_main.c - -App/app/app_main.c +App/algo/adc_health.h + +App/algo/adc_health.h - + -App/app/app_main.c->App/app/app_cfg.h - - - - - -App/app/app_main.h - -App/app/app_main.h - - - -App/app/app_main.c->App/app/app_main.h - - - - - -App/log/sys_log.h - -App/log/sys_log.h - - - -App/app/app_main.c->App/log/sys_log.h - - - - - -App/util/sys_clock.h - -App/util/sys_clock.h - - - -App/app/app_main.c->App/util/sys_clock.h - - - - - -App/util/task_watchdog.h - -App/util/task_watchdog.h - - - -App/app/app_main.c->App/util/task_watchdog.h - - - - - -Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - -Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - - - -App/app/app_main.c->Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - - - - - -Drivers/BSP/RS485/rs485.h - -Drivers/BSP/RS485/rs485.h - - - -App/app/app_main.c->Drivers/BSP/RS485/rs485.h - - - - - -Drivers/BSP/SD2506/sd2506.h - -Drivers/BSP/SD2506/sd2506.h - - - -App/app/app_main.c->Drivers/BSP/SD2506/sd2506.h - - - - - -Drivers/BSP/TPAFE5160/tpafe5160.h - -Drivers/BSP/TPAFE5160/tpafe5160.h - - - -App/app/app_main.c->Drivers/BSP/TPAFE5160/tpafe5160.h - - - - - -Drivers/BSP/dbg_log.h - -Drivers/BSP/dbg_log.h - - - -App/app/app_main.c->Drivers/BSP/dbg_log.h - - - - - -Inc/main.h - -Inc/main.h - - - -App/app/app_main.c->Inc/main.h - - - - - -Inc/stm32f4xx_it.h - -Inc/stm32f4xx_it.h - - - -App/app/app_main.c->Inc/stm32f4xx_it.h - - - - - -Inc/usart.h - -Inc/usart.h - - - -App/app/app_main.c->Inc/usart.h - - +App/algo/adc_health.c->App/algo/adc_health.h + + - + 外部/系统头文件 - -外部/系统头文件 + +外部/系统头文件 - - -App/app/app_main.h->外部/系统头文件 - - + + +App/algo/adc_health.h->外部/系统头文件 + + - + + +App/algo/adc_input.c + +App/algo/adc_input.c + + -App/app/sys_events.h - -App/app/sys_events.h +App/algo/adc_input.h + +App/algo/adc_input.h - - -Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h - -Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h + + +App/algo/adc_input.c->App/algo/adc_input.h + + - - -App/app/sys_events.h->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h - - - - - -App/cfg/cfg_file.c - -App/cfg/cfg_file.c - - - -App/cfg/cfg_file.h - -App/cfg/cfg_file.h - - - -App/cfg/cfg_file.c->App/cfg/cfg_file.h - - - - - -App/cfg/cfg_file_store.h - -App/cfg/cfg_file_store.h - - - -App/cfg/cfg_file.c->App/cfg/cfg_file_store.h - - - - - -App/cfg/cfg_file.c->App/log/sys_log.h - - - - - -App/cfg/cfg_file.c->Drivers/BSP/dbg_log.h - - - - - -App/cfg/cfg_file.c->外部/系统头文件 - - - - - -App/cfg/cfg_file.h->外部/系统头文件 - - - - - -App/cfg/cfg_file_store.h->外部/系统头文件 - - - - - -App/cfg/cfg_file_store_mcu.c - -App/cfg/cfg_file_store_mcu.c - - - -App/cfg/cfg_file_store_mcu.c->App/cfg/cfg_file_store.h - - - - - -App/fs/vol_lock.h - -App/fs/vol_lock.h - - - -App/cfg/cfg_file_store_mcu.c->App/fs/vol_lock.h - - - - - -Lib/FatFs/ff.h - -Lib/FatFs/ff.h - - - -App/cfg/cfg_file_store_mcu.c->Lib/FatFs/ff.h - - - - - -Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - -Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - - -App/cfg/cfg_file_store_mcu.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - - - - -App/cfg/cfg_file_store_mcu.c->外部/系统头文件 - - - - - -App/fs/vol_lock.c - -App/fs/vol_lock.c - - - -App/fs/vol_lock.c->App/fs/vol_lock.h - - - - - -Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - -Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - - -App/fs/vol_lock.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - - - - -App/fs/vol_lock.h->外部/系统头文件 - - - - - -App/log/console_log.c - -App/log/console_log.c - - - -App/log/console_log.c->App/app/sys_events.h - - - - - -App/log/console_log.h - -App/log/console_log.h - - - -App/log/console_log.c->App/log/console_log.h - - - - - -App/log/console_log.c->Inc/usart.h - - - - - -App/log/console_log.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - - - - -App/log/console_log.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - - - - -Middlewares/Third_Party/FreeRTOS/Source/include/task.h - -Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - - -App/log/console_log.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - - - - -App/log/console_log.c->外部/系统头文件 - - - - - -App/log/console_log.h->外部/系统头文件 - - - - - -App/log/sys_log.h->外部/系统头文件 - - - - - -App/log/sys_log_mcu.c - -App/log/sys_log_mcu.c - - - -App/log/sys_log_mcu.c->App/fs/vol_lock.h - - - - - -App/log/sys_log_mcu.c->App/log/sys_log.h - - - - - -App/log/sys_log_mcu.c->App/util/sys_clock.h - - - - - -App/log/sys_log_mcu.c->Drivers/BSP/dbg_log.h - - - - - -App/log/sys_log_mcu.c->Lib/FatFs/ff.h - - - - - -App/log/sys_log_mcu.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - - - - -App/log/sys_log_mcu.c->外部/系统头文件 - - - - - -App/sync/time_sync.c - -App/sync/time_sync.c - - - -App/sync/time_sync.c->App/app/sys_events.h - - - - - -App/sync/time_sync.h - -App/sync/time_sync.h - - - -App/sync/time_sync.c->App/sync/time_sync.h - - - - - -App/sync/time_sync.c->App/util/sys_clock.h - - - - - -App/sync/time_sync.c->App/util/task_watchdog.h - - - - - -Drivers/BSP/NET/net_socket.h - -Drivers/BSP/NET/net_socket.h - - - -App/sync/time_sync.c->Drivers/BSP/NET/net_socket.h - - - - - -App/sync/time_sync.c->Drivers/BSP/dbg_log.h - - - - - -App/sync/time_sync.c->外部/系统头文件 - - - - - -App/sync/time_sync.h->外部/系统头文件 - - - - - -App/task/adc_task.c - -App/task/adc_task.c - - - -App/task/adc_task.h - -App/task/adc_task.h - - - -App/task/adc_task.c->App/task/adc_task.h - - - - - -App/task/adc_task.c->Drivers/BSP/TPAFE5160/tpafe5160.h - - - - - -App/task/adc_task.c->Drivers/BSP/dbg_log.h - - - - - -App/task/adc_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - - - - -App/task/default_task.c - -App/task/default_task.c - - - -App/task/default_task.c->App/app/sys_events.h - - - - - -App/task/default_task.c->App/cfg/cfg_file.h - - - - - -App/task/default_task.c->App/fs/vol_lock.h - - - - - -App/task/default_task.c->App/log/sys_log.h - - - - - -App/task/default_task.h - -App/task/default_task.h - - - -App/task/default_task.c->App/task/default_task.h - - - - - -App/task/default_task.c->App/util/task_watchdog.h - - - - - -Drivers/BSP/GD5F2GQ5UE/nand_ftl.h - -Drivers/BSP/GD5F2GQ5UE/nand_ftl.h - - - -App/task/default_task.c->Drivers/BSP/GD5F2GQ5UE/nand_ftl.h - - - - - -Drivers/BSP/NETCFG/net_cfg.h - -Drivers/BSP/NETCFG/net_cfg.h - - - -App/task/default_task.c->Drivers/BSP/NETCFG/net_cfg.h - - - - - -App/task/default_task.c->Drivers/BSP/dbg_log.h - - - - - -App/task/default_task.c->Inc/main.h - - - - - -App/task/default_task.c->Lib/FatFs/ff.h - - - - - -App/task/default_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - - - - -App/task/default_task.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - - - - -App/task/default_task.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - - - - -test/board/test_config.h - -test/board/test_config.h - - - -App/task/default_task.c->test/board/test_config.h - - - - - -App/task/net_task.c - -App/task/net_task.c - - - -App/task/net_task.c->App/app/sys_events.h - - - - - -App/task/net_task.c->App/log/sys_log.h - - - - - -App/task/net_task.h - -App/task/net_task.h - - - -App/task/net_task.c->App/task/net_task.h - - - - - -App/task/net_task.c->App/util/task_watchdog.h - - - - - -Drivers/BSP/CH395F/ch395f.h - -Drivers/BSP/CH395F/ch395f.h - - - -App/task/net_task.c->Drivers/BSP/CH395F/ch395f.h - - - - - -Drivers/BSP/NET/net_select.h - -Drivers/BSP/NET/net_select.h - - - -App/task/net_task.c->Drivers/BSP/NET/net_select.h - - - - - -App/task/net_task.c->Drivers/BSP/NET/net_socket.h - - - - - -App/task/net_task.c->Drivers/BSP/NETCFG/net_cfg.h - - - - - -App/task/net_task.c->Drivers/BSP/dbg_log.h - - - - - -App/task/net_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - - - - -App/task/net_task.c->test/board/test_config.h - - - - - -App/task/net_task.c->外部/系统头文件 - - - - - -App/task/rs485_task.c - -App/task/rs485_task.c - - - -App/task/rs485_task.h - -App/task/rs485_task.h - - - -App/task/rs485_task.c->App/task/rs485_task.h - - - - - -App/task/rs485_task.c->Drivers/BSP/RS485/rs485.h - - - - - -App/task/rs485_task.c->Drivers/BSP/dbg_log.h - - - - - -App/task/rs485_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - - - - -App/task/ui_task.c - -App/task/ui_task.c - - - -App/task/ui_task.c->App/app/sys_events.h - - - - - -App/task/ui_task.h - -App/task/ui_task.h - - - -App/task/ui_task.c->App/task/ui_task.h - - - - - -App/task/ui_task.c->App/util/sys_clock.h - - - - - -App/task/ui_task.c->App/util/task_watchdog.h - - - - - -Drivers/BSP/UI/ui_menu.h - -Drivers/BSP/UI/ui_menu.h - - - -App/task/ui_task.c->Drivers/BSP/UI/ui_menu.h - - - - - -Drivers/BSP/UI/ui_net_sink.h - -Drivers/BSP/UI/ui_net_sink.h - - - -App/task/ui_task.c->Drivers/BSP/UI/ui_net_sink.h - - - - - -Drivers/BSP/UI/ui_port.h - -Drivers/BSP/UI/ui_port.h - - - -App/task/ui_task.c->Drivers/BSP/UI/ui_port.h - - - - - -Drivers/BSP/UI/ui_standby.h - -Drivers/BSP/UI/ui_standby.h - - - -App/task/ui_task.c->Drivers/BSP/UI/ui_standby.h - - - - - -App/task/ui_task.c->Drivers/BSP/dbg_log.h - - - - - -App/task/ui_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - - - - -test/board/net_latency_test.h - -test/board/net_latency_test.h - - - -App/task/ui_task.c->test/board/net_latency_test.h - - - - - -App/task/ui_task.c->test/board/test_config.h - - - - - -App/task/ui_task.c->外部/系统头文件 - - - - - -App/util/sys_clock.c - -App/util/sys_clock.c - - - -App/util/sys_clock.c->App/util/sys_clock.h - - - - - -App/util/sys_clock.c->Drivers/BSP/dbg_log.h - - - - - -App/util/sys_clock.c->Inc/main.h - - - - - -App/util/sys_clock.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - - - - -App/util/sys_clock.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - - - - -App/util/sys_clock.c->外部/系统头文件 - - - - - -App/util/sys_clock.h->Drivers/BSP/SD2506/sd2506.h - - - - - -App/util/sys_clock.h->外部/系统头文件 - - - - - -App/util/task_watchdog.c - -App/util/task_watchdog.c - - - -App/util/task_watchdog.c->App/log/console_log.h - - - - - -App/util/task_watchdog.c->App/log/sys_log.h - - - - - -App/util/task_watchdog.c->App/util/task_watchdog.h - - - - - -Drivers/BSP/NET/lftpd/lftpd.h - -Drivers/BSP/NET/lftpd/lftpd.h - - - -App/util/task_watchdog.c->Drivers/BSP/NET/lftpd/lftpd.h - - - - - -App/util/task_watchdog.c->Drivers/BSP/dbg_log.h - - - - - -Inc/iwdg.h - -Inc/iwdg.h - - - -App/util/task_watchdog.c->Inc/iwdg.h - - - - - -App/util/task_watchdog.c->Inc/main.h - - - - - -App/util/task_watchdog.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - - - - -App/util/task_watchdog.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - - - - -App/util/task_watchdog.c->外部/系统头文件 - - - - - -App/util/task_watchdog.h->外部/系统头文件 - - - - - -Drivers/BSP/CH395F/ch395f.c - -Drivers/BSP/CH395F/ch395f.c - - - -Drivers/BSP/CH395F/ch395f.c->Drivers/BSP/CH395F/ch395f.h - - - - - -Drivers/BSP/CH395F/ch395f.c->Drivers/BSP/dbg_log.h - - - - - -Inc/spi.h - -Inc/spi.h - - - -Drivers/BSP/CH395F/ch395f.c->Inc/spi.h - - - - - -Drivers/BSP/CH395F/ch395f.c->Inc/usart.h - - - - - -Drivers/BSP/CH395F/ch395f.c->外部/系统头文件 - - - - - -Drivers/BSP/CH395F/ch395f.h->Inc/main.h - - - - - -Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c - -Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c - - - -Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c->Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - - - - - -Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c->Drivers/BSP/dbg_log.h - - - - - -Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c->外部/系统头文件 - - - - - -Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h->Inc/main.h - - - - - -Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h->外部/系统头文件 - - - - - -Drivers/BSP/GD5F2GQ5UE/nand_ftl.c - -Drivers/BSP/GD5F2GQ5UE/nand_ftl.c - - - -Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - - - - - -Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Drivers/BSP/dbg_log.h - - - - - -Lib/FatFs/diskio.h - -Lib/FatFs/diskio.h - - - -Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Lib/FatFs/diskio.h - - - - - -Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Lib/FatFs/ff.h - - - - - -Lib/dhara/map.h - -Lib/dhara/map.h - - - -Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Lib/dhara/map.h - - - - - -Lib/dhara/nand.h - -Lib/dhara/nand.h - - - -Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Lib/dhara/nand.h - - - - - -Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->外部/系统头文件 - - - - - -Drivers/BSP/GD5F2GQ5UE/nand_ftl.h->外部/系统头文件 - - - - - -Drivers/BSP/NET/net_config.h - -Drivers/BSP/NET/net_config.h - - - -Drivers/BSP/NET/net_select.c - -Drivers/BSP/NET/net_select.c - - - -Drivers/BSP/NET/net_select.c->Drivers/BSP/CH395F/ch395f.h - - - - - -Drivers/BSP/NET/net_select.c->Drivers/BSP/NET/net_select.h - - - - - -Drivers/BSP/NET/net_select.c->Drivers/BSP/NET/net_socket.h - - - - - -Drivers/BSP/NET/net_select.c->Inc/main.h - - - - - -Drivers/BSP/NET/net_select.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - - - - -Drivers/BSP/NET/net_select.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - - - - -Drivers/BSP/NET/net_select.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - - - - -Drivers/BSP/NET/net_select.c->外部/系统头文件 - - - - - -Drivers/BSP/NET/net_select.h->Drivers/BSP/NET/net_config.h - - - - - -Drivers/BSP/NET/net_types.h - -Drivers/BSP/NET/net_types.h - - - -Drivers/BSP/NET/net_select.h->Drivers/BSP/NET/net_types.h - - - - - -Drivers/BSP/NET/net_socket.c - -Drivers/BSP/NET/net_socket.c - - - -Drivers/BSP/NET/net_socket.c->Drivers/BSP/CH395F/ch395f.h - - - - - -Drivers/BSP/NET/net_socket.c->Drivers/BSP/NET/net_socket.h - - - - - -Drivers/BSP/NET/net_socket.c->Drivers/BSP/dbg_log.h - - - - - -Drivers/BSP/NET/net_socket.c->Inc/main.h - - - - - -Drivers/BSP/NET/net_socket.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - - - - -Drivers/BSP/NET/net_socket.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - - - - -Drivers/BSP/NET/net_socket.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - - - - -Drivers/BSP/NET/net_socket.c->test/board/net_latency_test.h - - - - - -Drivers/BSP/NET/net_socket.c->test/board/test_config.h - - - - - -Drivers/BSP/NET/net_socket.c->外部/系统头文件 - - - - - -Drivers/BSP/NET/net_socket.h->Drivers/BSP/NET/net_types.h - - - - - -Drivers/BSP/NET/net_socket.h->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - - - - -Drivers/BSP/NET/net_socket.h->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - - - - -Drivers/BSP/NET/net_types.h->Drivers/BSP/NET/net_config.h - - - - - -Drivers/BSP/NET/net_types.h->外部/系统头文件 - - - - - -Drivers/BSP/NETCFG/net_cfg.c - -Drivers/BSP/NETCFG/net_cfg.c - - - -Drivers/BSP/NETCFG/net_cfg.c->App/cfg/cfg_file.h - - - - - -Drivers/BSP/NETCFG/net_cfg.c->Drivers/BSP/NETCFG/net_cfg.h - - - - - -Drivers/BSP/NETCFG/net_cfg.c->Drivers/BSP/dbg_log.h - - - - - -Drivers/BSP/NETCFG/net_cfg.c->外部/系统头文件 - - - - - -Drivers/BSP/NETCFG/net_cfg.h->外部/系统头文件 - - - - - -Drivers/BSP/RS485/rs485.c - -Drivers/BSP/RS485/rs485.c - - - -Drivers/BSP/RS485/rs485.c->Drivers/BSP/RS485/rs485.h - - - - - -Drivers/BSP/RS485/rs485.h->Inc/main.h - - - - - -Drivers/BSP/SD2506/sd2506.c - -Drivers/BSP/SD2506/sd2506.c - - - -Drivers/BSP/SD2506/sd2506.c->Drivers/BSP/SD2506/sd2506.h - - - - - -Drivers/BSP/SD2506/sd2506.c->Drivers/BSP/dbg_log.h - - - - - -Inc/i2c.h - -Inc/i2c.h - - - -Drivers/BSP/SD2506/sd2506.c->Inc/i2c.h - - - - - -Drivers/BSP/SD2506/sd2506.c->外部/系统头文件 - - - - - -Drivers/BSP/SD2506/sd2506.h->Inc/main.h - - - - - -Drivers/BSP/TPAFE5160/tpafe5160.c - -Drivers/BSP/TPAFE5160/tpafe5160.c - - - -Drivers/BSP/TPAFE5160/tpafe5160.c->Drivers/BSP/TPAFE5160/tpafe5160.h - - - - - -Drivers/BSP/TPAFE5160/tpafe5160.c->Drivers/BSP/dbg_log.h - - - - - -Drivers/BSP/TPAFE5160/tpafe5160.c->Inc/main.h - - - - - -Drivers/BSP/TPAFE5160/tpafe5160.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - - - - -Drivers/BSP/TPAFE5160/tpafe5160.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - - - - -Drivers/BSP/TPAFE5160/tpafe5160.c->外部/系统头文件 - - - - - -Drivers/BSP/TPAFE5160/tpafe5160.h->Inc/main.h - - - - - -Drivers/BSP/UI/ui_content_page.c - -Drivers/BSP/UI/ui_content_page.c - - - -Drivers/BSP/UI/ui_content_page.h - -Drivers/BSP/UI/ui_content_page.h - - - -Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_content_page.h - - - - - -Drivers/BSP/UI/ui_deco.h - -Drivers/BSP/UI/ui_deco.h - - - -Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_deco.h - - - - - -Drivers/BSP/UI/ui_menu_view.h - -Drivers/BSP/UI/ui_menu_view.h - - - -Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_menu_view.h - - - - - -Drivers/BSP/UI/ui_page_ac.h - -Drivers/BSP/UI/ui_page_ac.h - - - -Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_ac.h - - - - - -Drivers/BSP/UI/ui_page_dc.h - -Drivers/BSP/UI/ui_page_dc.h - - - -Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_dc.h - - - - - -Drivers/BSP/UI/ui_page_device_info.h - -Drivers/BSP/UI/ui_page_device_info.h - - - -Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_device_info.h - - - - - -Drivers/BSP/UI/ui_page_net_cfg.h - -Drivers/BSP/UI/ui_page_net_cfg.h - - - -Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_net_cfg.h - - - - - -Drivers/BSP/UI/ui_page_password.h - -Drivers/BSP/UI/ui_page_password.h - - - -Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_password.h - - - - - -Drivers/BSP/UI/ui_page_runlog.h - -Drivers/BSP/UI/ui_page_runlog.h - - - -Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_runlog.h - - - - + -Drivers/BSP/UI/ui_page_selftest.h - -Drivers/BSP/UI/ui_page_selftest.h - - - -Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_selftest.h - - - - - -Drivers/BSP/UI/ui_pwd_def.h - -Drivers/BSP/UI/ui_pwd_def.h - - - -Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_pwd_def.h - - - - - -Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_standby.h - - - - - -Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/dbg_log.h - - - - - -Lib/lvgl/lvgl.h - -Lib/lvgl/lvgl.h - - - -Drivers/BSP/UI/ui_content_page.c->Lib/lvgl/lvgl.h - - - - - -Drivers/BSP/UI/ui_content_page.c->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_content_page.h->Lib/lvgl/lvgl.h - - - - - -Drivers/BSP/UI/ui_content_page.h->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_deco.c - -Drivers/BSP/UI/ui_deco.c - - - -Drivers/BSP/UI/ui_deco.c->Drivers/BSP/UI/ui_deco.h - - - - - -Drivers/BSP/UI/ui_deco.c->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_deco.h->Lib/lvgl/lvgl.h - - - - - -Drivers/BSP/UI/ui_font_cn_12.c - -Drivers/BSP/UI/ui_font_cn_12.c - - - -Drivers/BSP/UI/ui_font_cn_12.c->Lib/lvgl/lvgl.h - - - - - -Drivers/BSP/UI/ui_menu.c - -Drivers/BSP/UI/ui_menu.c - - - -Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_content_page.h - - - - - -Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_menu.h - - - - - -Drivers/BSP/UI/ui_menu_tab.h - -Drivers/BSP/UI/ui_menu_tab.h - - - -Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_menu_tab.h - - - - - -Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_menu_view.h - - - - - -Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_page_password.h - - - - - -Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_pwd_def.h - - - - - -Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_standby.h - - - - - -Drivers/BSP/UI/ui_menu.c->Drivers/BSP/dbg_log.h - - - - - -Drivers/BSP/UI/ui_menu.c->Lib/lvgl/lvgl.h - - - - - -Drivers/BSP/UI/ui_menu.c->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_menu_tab.c - -Drivers/BSP/UI/ui_menu_tab.c - - - -Drivers/BSP/UI/ui_menu_tab.c->Drivers/BSP/UI/ui_menu_tab.h - - - - - -Drivers/BSP/UI/ui_menu_tab.h->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_menu_view.c - -Drivers/BSP/UI/ui_menu_view.c - - - -Drivers/BSP/UI/ui_menu_view.c->Drivers/BSP/UI/ui_deco.h - - - - - -Drivers/BSP/UI/ui_menu_view.c->Drivers/BSP/UI/ui_menu_tab.h - - - - - -Drivers/BSP/UI/ui_menu_view.c->Drivers/BSP/UI/ui_menu_view.h - - - - - -Drivers/BSP/UI/ui_menu_view.c->Drivers/BSP/dbg_log.h - - - - - -Drivers/BSP/UI/ui_menu_view.c->Lib/lvgl/lvgl.h - - - - - -Drivers/BSP/UI/ui_menu_view.c->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_menu_view.h->Lib/lvgl/lvgl.h - - - - - -Drivers/BSP/UI/ui_menu_view.h->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_net_sink.c - -Drivers/BSP/UI/ui_net_sink.c - - - -Drivers/BSP/UI/ui_net_sink.c->Drivers/BSP/NET/net_socket.h - - - - - -Drivers/BSP/UI/ui_net_sink.c->Drivers/BSP/NET/net_types.h - - - - - -Drivers/BSP/UI/ui_net_sink.c->Drivers/BSP/UI/ui_net_sink.h - - - - - -Drivers/BSP/UI/ui_net_sink_backend.h - -Drivers/BSP/UI/ui_net_sink_backend.h - - - -Drivers/BSP/UI/ui_net_sink.c->Drivers/BSP/UI/ui_net_sink_backend.h - - - - - -Drivers/BSP/UI/ui_net_sink.c->Drivers/BSP/dbg_log.h - - - - - -Drivers/BSP/UI/ui_net_sink.c->test/board/net_latency_test.h - - - - - -Drivers/BSP/UI/ui_net_sink.c->test/board/test_config.h - - - - - -Drivers/BSP/UI/ui_net_sink.c->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_net_sink.h->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_net_sink_backend.h->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_net_sink_backend_mcu.c - -Drivers/BSP/UI/ui_net_sink_backend_mcu.c - - - -Drivers/BSP/UI/ui_net_sink_backend_mcu.c->Drivers/BSP/UI/ui_net_sink_backend.h - - - - - -Drivers/BSP/UI/ui_net_sink_backend_mcu.c->Drivers/BSP/UI/ui_port.h - - - - - -Drivers/BSP/UI/ui_net_sink_backend_mcu.c->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_page_ac.c - -Drivers/BSP/UI/ui_page_ac.c - - - -Drivers/BSP/UI/ui_page_ac.c->Drivers/BSP/UI/ui_page_ac.h - - - - - -Drivers/BSP/UI/ui_page_ac.h->Drivers/BSP/UI/ui_content_page.h - - - - - -Drivers/BSP/UI/ui_page_dc.c - -Drivers/BSP/UI/ui_page_dc.c - - - -Drivers/BSP/UI/ui_page_dc.c->Drivers/BSP/UI/ui_page_dc.h - - - - - -Drivers/BSP/UI/ui_page_dc.h->Drivers/BSP/UI/ui_content_page.h - - - - - -Drivers/BSP/UI/ui_page_device_info.c - -Drivers/BSP/UI/ui_page_device_info.c - - - -Drivers/BSP/UI/ui_page_device_info.c->Drivers/BSP/UI/ui_page_device_info.h - - - - - -Drivers/BSP/UI/ui_page_device_info.h->Drivers/BSP/UI/ui_content_page.h - - - - - -Drivers/BSP/UI/ui_page_net_cfg.c - -Drivers/BSP/UI/ui_page_net_cfg.c - - - -Drivers/BSP/UI/ui_page_net_cfg.c->Drivers/BSP/NETCFG/net_cfg.h - - - - - -Drivers/BSP/UI/ui_page_net_cfg.c->Drivers/BSP/UI/ui_page_net_cfg.h - - - - - -Drivers/BSP/UI/ui_page_net_cfg.c->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_page_net_cfg.h->Drivers/BSP/UI/ui_content_page.h - - - - - -Drivers/BSP/UI/ui_page_password.c - -Drivers/BSP/UI/ui_page_password.c - - - -Drivers/BSP/UI/ui_page_password.c->Drivers/BSP/UI/ui_page_password.h - - - - - -Drivers/BSP/UI/ui_page_password.c->Drivers/BSP/UI/ui_pwd_def.h - - - - - -Drivers/BSP/UI/ui_page_password.c->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_page_password.h->Drivers/BSP/UI/ui_content_page.h - - - - - -Drivers/BSP/UI/ui_page_runlog.c - -Drivers/BSP/UI/ui_page_runlog.c - - - -Drivers/BSP/UI/ui_page_runlog.c->App/log/sys_log.h - - - - - -Drivers/BSP/UI/ui_page_runlog.c->Drivers/BSP/UI/ui_page_runlog.h - - - - - -Drivers/BSP/UI/ui_page_runlog.c->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_page_runlog.h->Drivers/BSP/UI/ui_content_page.h - - - - - -Drivers/BSP/UI/ui_page_selftest.c - -Drivers/BSP/UI/ui_page_selftest.c - - - -Drivers/BSP/UI/ui_page_selftest.c->App/log/sys_log.h - - - - - -Drivers/BSP/UI/ui_page_selftest.c->Drivers/BSP/UI/ui_page_selftest.h - - - - - -Drivers/BSP/UI/ui_page_selftest.c->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_page_selftest.h->Drivers/BSP/UI/ui_content_page.h - - - - - -Drivers/BSP/UI/ui_port.c - -Drivers/BSP/UI/ui_port.c - - - -Drivers/BSP/UI/ui_port.c->Drivers/BSP/UI/ui_port.h - - - - - -Drivers/BSP/UI/ui_port.c->Drivers/BSP/dbg_log.h - - - - - -Drivers/BSP/UI/ui_port.c->Lib/lvgl/lvgl.h - - - - - -Drivers/BSP/UI/ui_port.c->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_port.h->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_pwd_def.h->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_standby.c - -Drivers/BSP/UI/ui_standby.c - - - -Drivers/BSP/UI/ui_standby.c->Drivers/BSP/UI/ui_menu_view.h - - - - - -Drivers/BSP/UI/ui_standby.c->Drivers/BSP/UI/ui_standby.h - - - - - -Drivers/BSP/UI/ui_standby.c->Drivers/BSP/dbg_log.h - - - - - -Drivers/BSP/UI/ui_standby.c->Lib/lvgl/lvgl.h - - - - - -Drivers/BSP/UI/ui_standby.c->外部/系统头文件 - - - - - -Drivers/BSP/UI/ui_standby.h->外部/系统头文件 - - - - - -Drivers/BSP/dbg_cfg.h - -Drivers/BSP/dbg_cfg.h - - - -Drivers/BSP/dbg_log.h->App/log/console_log.h - - - - - -Drivers/BSP/dbg_log.h->App/util/sys_clock.h - - - - - -Drivers/BSP/dbg_log.h->Drivers/BSP/dbg_cfg.h - - - - - -Drivers/BSP/dbg_log.h->外部/系统头文件 - - - - - -Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h - -Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h - - - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h - - - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc.h - - - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_cortex.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_cortex.h - - - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_dma.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_dma.h - - - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_exti.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_exti.h - - - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_flash.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_flash.h - - - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_gpio.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_gpio.h - - - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_i2c.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_i2c.h - - - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_iwdg.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_iwdg.h - - - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_pwr.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_pwr.h - - - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_rcc.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_rcc.h - - - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_spi.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_spi.h - - - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_tim.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_tim.h - - - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_uart.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_uart.h - - - -Inc/FreeRTOSConfig.h - -Inc/FreeRTOSConfig.h - - - -Inc/FreeRTOSConfig.h->外部/系统头文件 - - +Drivers/BSP/TPAFE5160/tpafe5160.h + +Drivers/BSP/TPAFE5160/tpafe5160.h + + + +App/algo/adc_input.c->Drivers/BSP/TPAFE5160/tpafe5160.h + + - + Inc/adc.h - -Inc/adc.h + +Inc/adc.h + + + +App/algo/adc_input.c->Inc/adc.h + + + + + +App/algo/adc_input.c->外部/系统头文件 + + + + + +App/algo/adc_input.h->外部/系统头文件 + + + + + +App/algo/phasor.c + +App/algo/phasor.c + + + +App/algo/phasor.h + +App/algo/phasor.h + + + +App/algo/phasor.c->App/algo/phasor.h + + + + + +App/algo/sample_ring.h + +App/algo/sample_ring.h + + + +App/algo/phasor.c->App/algo/sample_ring.h + + + + + +App/algo/phasor.c->外部/系统头文件 + + + + + +App/algo/phasor.h->App/algo/adc_input.h + + + + + +App/algo/phasor.h->外部/系统头文件 + + + + + +App/algo/prot_elm.c + +App/algo/prot_elm.c + + + +App/algo/prot_elm.c->App/algo/phasor.h + + + + + +App/algo/prot_elm.h + +App/algo/prot_elm.h + + + +App/algo/prot_elm.c->App/algo/prot_elm.h + + + + + +App/algo/prot_set.h + +App/algo/prot_set.h + + + +App/algo/prot_elm.c->App/algo/prot_set.h + + + + + +App/algo/prot_elm.c->外部/系统头文件 + + + + + +App/algo/prot_elm.h->外部/系统头文件 + + + + + +App/algo/prot_set.c + +App/algo/prot_set.c + + + +App/algo/prot_set.c->App/algo/prot_set.h + + + + + +App/algo/prot_set.h->外部/系统头文件 + + + + + +App/algo/sample_ring.c + +App/algo/sample_ring.c + + + +App/algo/sample_ring.c->App/algo/sample_ring.h + + + + + +App/algo/sample_ring.c->外部/系统头文件 + + + + + +App/algo/sample_ring.h->App/algo/adc_input.h + + + + + +App/algo/sample_ring.h->外部/系统头文件 + + + + + +App/app/app_cfg.h + +App/app/app_cfg.h + + + +App/app/app_main.c + +App/app/app_main.c + + + +App/app/app_main.c->App/algo/adc_input.h + + + + + +App/app/app_main.c->App/algo/phasor.h + + + + + +App/app/app_main.c->App/algo/prot_elm.h + + + + + +App/app/app_main.c->App/algo/prot_set.h + + + + + +App/app/app_main.c->App/app/app_cfg.h + + + + + +App/app/app_main.h + +App/app/app_main.h + + + +App/app/app_main.c->App/app/app_main.h + + + + + +App/log/sys_log.h + +App/log/sys_log.h + + + +App/app/app_main.c->App/log/sys_log.h + + + + + +App/util/sys_clock.h + +App/util/sys_clock.h + + + +App/app/app_main.c->App/util/sys_clock.h + + + + + +App/util/task_watchdog.h + +App/util/task_watchdog.h + + + +App/app/app_main.c->App/util/task_watchdog.h + + + + + +Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h + +Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h + + + +App/app/app_main.c->Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h + + + + + +Drivers/BSP/RS485/rs485.h + +Drivers/BSP/RS485/rs485.h + + + +App/app/app_main.c->Drivers/BSP/RS485/rs485.h + + + + + +Drivers/BSP/SD2506/sd2506.h + +Drivers/BSP/SD2506/sd2506.h + + + +App/app/app_main.c->Drivers/BSP/SD2506/sd2506.h + + + + + +App/app/app_main.c->Drivers/BSP/TPAFE5160/tpafe5160.h + + + + + +Drivers/BSP/dbg_log.h + +Drivers/BSP/dbg_log.h + + + +App/app/app_main.c->Drivers/BSP/dbg_log.h + + + + + +Inc/main.h + +Inc/main.h + + + +App/app/app_main.c->Inc/main.h + + + + + +Inc/stm32f4xx_it.h + +Inc/stm32f4xx_it.h + + + +App/app/app_main.c->Inc/stm32f4xx_it.h + + + + + +Inc/tim.h + +Inc/tim.h + + + +App/app/app_main.c->Inc/tim.h + + + + + +Inc/usart.h + +Inc/usart.h + + + +App/app/app_main.c->Inc/usart.h + + + + + +test/board/test_config.h + +test/board/test_config.h + + + +App/app/app_main.c->test/board/test_config.h + + + + + +App/app/app_main.h->外部/系统头文件 + + + + + +App/app/sys_events.h + +App/app/sys_events.h + + + +Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h + +Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h + + + +App/app/sys_events.h->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h + + + + + +App/cfg/cfg_file.c + +App/cfg/cfg_file.c + + + +App/cfg/cfg_file.h + +App/cfg/cfg_file.h + + + +App/cfg/cfg_file.c->App/cfg/cfg_file.h + + + + + +App/cfg/cfg_file_store.h + +App/cfg/cfg_file_store.h + + + +App/cfg/cfg_file.c->App/cfg/cfg_file_store.h + + + + + +App/cfg/cfg_file.c->App/log/sys_log.h + + + + + +App/cfg/cfg_file.c->Drivers/BSP/dbg_log.h + + + + + +App/cfg/cfg_file.c->外部/系统头文件 + + + + + +App/cfg/cfg_file.h->外部/系统头文件 + + + + + +App/cfg/cfg_file_store.h->外部/系统头文件 + + + + + +App/cfg/cfg_file_store_mcu.c + +App/cfg/cfg_file_store_mcu.c + + + +App/cfg/cfg_file_store_mcu.c->App/cfg/cfg_file_store.h + + + + + +App/fs/vol_lock.h + +App/fs/vol_lock.h + + + +App/cfg/cfg_file_store_mcu.c->App/fs/vol_lock.h + + + + + +Lib/FatFs/ff.h + +Lib/FatFs/ff.h + + + +App/cfg/cfg_file_store_mcu.c->Lib/FatFs/ff.h + + + + + +Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h + +Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h + + + +App/cfg/cfg_file_store_mcu.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h + + + + + +App/cfg/cfg_file_store_mcu.c->外部/系统头文件 + + + + + +App/fs/vol_lock.c + +App/fs/vol_lock.c + + + +App/fs/vol_lock.c->App/fs/vol_lock.h + + + + + +Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + +Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + + + +App/fs/vol_lock.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + + + + + +App/fs/vol_lock.h->外部/系统头文件 + + + + + +App/log/console_log.c + +App/log/console_log.c + + + +App/log/console_log.c->App/app/sys_events.h + + + + + +App/log/console_log.h + +App/log/console_log.h + + + +App/log/console_log.c->App/log/console_log.h + + + + + +App/log/console_log.c->Inc/usart.h + + + + + +App/log/console_log.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + + + + + +App/log/console_log.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h + + + + + +Middlewares/Third_Party/FreeRTOS/Source/include/task.h + +Middlewares/Third_Party/FreeRTOS/Source/include/task.h + + + +App/log/console_log.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h + + + + + +App/log/console_log.c->外部/系统头文件 + + + + + +App/log/console_log.h->外部/系统头文件 + + + + + +App/log/sys_log.h->外部/系统头文件 + + + + + +App/log/sys_log_mcu.c + +App/log/sys_log_mcu.c + + + +App/log/sys_log_mcu.c->App/fs/vol_lock.h + + + + + +App/log/sys_log_mcu.c->App/log/sys_log.h + + + + + +App/log/sys_log_mcu.c->App/util/sys_clock.h + + + + + +App/log/sys_log_mcu.c->Drivers/BSP/dbg_log.h + + + + + +App/log/sys_log_mcu.c->Lib/FatFs/ff.h + + + + + +App/log/sys_log_mcu.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + + + + + +App/log/sys_log_mcu.c->外部/系统头文件 + + + + + +App/protocol/iec104/iec104_asdu.c + +App/protocol/iec104/iec104_asdu.c + + + +App/protocol/iec104/iec104_asdu.h + +App/protocol/iec104/iec104_asdu.h + + + +App/protocol/iec104/iec104_asdu.c->App/protocol/iec104/iec104_asdu.h + + + + + +App/protocol/iec104/iec104_asdu.c->外部/系统头文件 + + + + + +App/protocol/iec104/iec104_asdu.h->外部/系统头文件 + + + + + +App/protocol/iec104/iec104_link.c + +App/protocol/iec104/iec104_link.c + + + +App/protocol/iec104/iec104_link.c->App/protocol/iec104/iec104_asdu.h + + + + + +App/protocol/iec104/iec104_link.h + +App/protocol/iec104/iec104_link.h + + + +App/protocol/iec104/iec104_link.c->App/protocol/iec104/iec104_link.h + + + + + +App/protocol/iec104/iec104_link.c->外部/系统头文件 + + + + + +App/protocol/iec104/iec104_pts.h + +App/protocol/iec104/iec104_pts.h + + + +App/protocol/iec104/iec104_link.h->App/protocol/iec104/iec104_pts.h + + + + + +App/protocol/iec104/iec104_link.h->外部/系统头文件 + + + + + +App/protocol/iec104/iec104_pts.c + +App/protocol/iec104/iec104_pts.c + + + +App/protocol/iec104/iec104_pts.c->App/algo/adc_input.h + + + + + +App/protocol/iec104/iec104_pts.c->App/algo/phasor.h + + + + + +App/protocol/iec104/iec104_pts.c->App/algo/prot_elm.h + + + + + +App/protocol/iec104/iec104_pts.c->App/algo/prot_set.h + + + + + +App/protocol/iec104/iec104_pts.c->App/protocol/iec104/iec104_asdu.h + + + + + +App/protocol/iec104/iec104_pts.c->App/protocol/iec104/iec104_pts.h + + + + + +App/protocol/iec104/iec104_pts.c->外部/系统头文件 + + + + + +App/protocol/iec104/iec104_pts.h->外部/系统头文件 + + + + + +App/sync/time_sync.c + +App/sync/time_sync.c + + + +App/sync/time_sync.c->App/app/sys_events.h + + + + + +App/sync/time_sync.h + +App/sync/time_sync.h + + + +App/sync/time_sync.c->App/sync/time_sync.h + + + + + +App/sync/time_sync.c->App/util/sys_clock.h + + + + + +App/sync/time_sync.c->App/util/task_watchdog.h + + + + + +Drivers/BSP/NET/net_socket.h + +Drivers/BSP/NET/net_socket.h + + + +App/sync/time_sync.c->Drivers/BSP/NET/net_socket.h + + + + + +App/sync/time_sync.c->Drivers/BSP/dbg_log.h + + + + + +App/sync/time_sync.c->外部/系统头文件 + + + + + +App/sync/time_sync.h->外部/系统头文件 + + + + + +App/task/adc_task.c + +App/task/adc_task.c + + + +App/task/adc_task.c->App/algo/adc_health.h + + + + + +App/task/adc_task.c->App/algo/adc_input.h + + + + + +App/task/adc_task.c->App/algo/phasor.h + + + + + +App/task/adc_task.c->App/algo/sample_ring.h + + + + + +App/task/adc_task.c->App/log/console_log.h + + + + + +App/task/adc_task.c->App/log/sys_log.h + + + + + +App/task/adc_task.h + +App/task/adc_task.h + + + +App/task/adc_task.c->App/task/adc_task.h + + + + + +App/util/task_prof.h + +App/util/task_prof.h + + + +App/task/adc_task.c->App/util/task_prof.h + + + + + +App/task/adc_task.c->App/util/task_watchdog.h + + + + + +App/task/adc_task.c->Drivers/BSP/TPAFE5160/tpafe5160.h + + + + + +App/task/adc_task.c->Drivers/BSP/dbg_log.h + + + + + +App/task/adc_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + + + + + +App/task/default_task.c + +App/task/default_task.c + + + +App/task/default_task.c->App/app/sys_events.h + + + + + +App/task/default_task.c->App/cfg/cfg_file.h + + + + + +App/task/default_task.c->App/fs/vol_lock.h + + + + + +App/task/default_task.c->App/log/sys_log.h + + + + + +App/task/default_task.h + +App/task/default_task.h + + + +App/task/default_task.c->App/task/default_task.h + + + + + +App/task/default_task.c->App/util/task_prof.h + + + + + +App/task/default_task.c->App/util/task_watchdog.h + + + + + +Drivers/BSP/GD5F2GQ5UE/nand_ftl.h + +Drivers/BSP/GD5F2GQ5UE/nand_ftl.h + + + +App/task/default_task.c->Drivers/BSP/GD5F2GQ5UE/nand_ftl.h + + + + + +Drivers/BSP/NETCFG/net_cfg.h + +Drivers/BSP/NETCFG/net_cfg.h + + + +App/task/default_task.c->Drivers/BSP/NETCFG/net_cfg.h + + + + + +App/task/default_task.c->Drivers/BSP/dbg_log.h + + + + + +App/task/default_task.c->Inc/main.h + + + + + +App/task/default_task.c->Lib/FatFs/ff.h + + + + + +App/task/default_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + + + + + +App/task/default_task.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h + + + + + +App/task/default_task.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h + + + + + +App/task/default_task.c->test/board/test_config.h + + + + + +App/task/iec104_serv.c + +App/task/iec104_serv.c + + + +App/task/iec104_serv.c->App/app/sys_events.h + + + + + +App/task/iec104_serv.c->App/protocol/iec104/iec104_link.h + + + + + +App/task/iec104_serv.c->App/protocol/iec104/iec104_pts.h + + + + + +App/task/iec104_serv.h + +App/task/iec104_serv.h + + + +App/task/iec104_serv.c->App/task/iec104_serv.h + + + + + +App/task/iec104_serv.c->App/util/sys_clock.h + + + + + +App/task/iec104_serv.c->App/util/task_watchdog.h + + + + + +App/task/iec104_serv.c->Drivers/BSP/NET/net_socket.h + + + + + +App/task/iec104_serv.c->Drivers/BSP/dbg_log.h + + + + + +App/task/iec104_serv.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h + + + + + +App/task/iec104_serv.c->外部/系统头文件 + + + + + +App/task/net_task.c + +App/task/net_task.c + + + +App/task/net_task.c->App/app/sys_events.h + + + + + +App/task/net_task.c->App/log/sys_log.h + + + + + +App/task/net_task.h + +App/task/net_task.h + + + +App/task/net_task.c->App/task/net_task.h + + + + + +App/task/net_task.c->App/util/task_prof.h + + + + + +App/task/net_task.c->App/util/task_watchdog.h + + + + + +Drivers/BSP/CH395F/ch395f.h + +Drivers/BSP/CH395F/ch395f.h + + + +App/task/net_task.c->Drivers/BSP/CH395F/ch395f.h + + + + + +Drivers/BSP/NET/net_select.h + +Drivers/BSP/NET/net_select.h + + + +App/task/net_task.c->Drivers/BSP/NET/net_select.h + + + + + +App/task/net_task.c->Drivers/BSP/NET/net_socket.h + + + + + +App/task/net_task.c->Drivers/BSP/NETCFG/net_cfg.h + + + + + +App/task/net_task.c->Drivers/BSP/dbg_log.h + + + + + +App/task/net_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + + + + + +App/task/net_task.c->test/board/test_config.h + + + + + +App/task/net_task.c->外部/系统头文件 + + + + + +App/task/protect_task.c + +App/task/protect_task.c + + + +App/task/protect_task.c->App/algo/adc_health.h + + + + + +App/task/protect_task.c->App/algo/prot_elm.h + + + + + +App/task/protect_task.c->App/algo/prot_set.h + + + + + +App/task/protect_task.h + +App/task/protect_task.h + + + +App/task/protect_task.c->App/task/protect_task.h + + + + + +App/task/protect_task.c->App/util/task_watchdog.h + + + + + +App/task/protect_task.c->Drivers/BSP/dbg_log.h + + + + + +App/task/protect_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h + + + + + +App/task/rs485_task.c + +App/task/rs485_task.c + + + +App/task/rs485_task.h + +App/task/rs485_task.h + + + +App/task/rs485_task.c->App/task/rs485_task.h + + + + + +App/task/rs485_task.c->Drivers/BSP/RS485/rs485.h + + + + + +App/task/rs485_task.c->Drivers/BSP/dbg_log.h + + + + + +App/task/rs485_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + + + + + +App/task/ui_task.c + +App/task/ui_task.c + + + +App/task/ui_task.c->App/app/sys_events.h + + + + + +App/task/ui_task.h + +App/task/ui_task.h + + + +App/task/ui_task.c->App/task/ui_task.h + + + + + +App/task/ui_task.c->App/util/sys_clock.h + + + + + +App/task/ui_task.c->App/util/task_prof.h + + + + + +App/task/ui_task.c->App/util/task_watchdog.h + + + + + +Drivers/BSP/UI/ui_menu.h + +Drivers/BSP/UI/ui_menu.h + + + +App/task/ui_task.c->Drivers/BSP/UI/ui_menu.h + + + + + +Drivers/BSP/UI/ui_net_sink.h + +Drivers/BSP/UI/ui_net_sink.h + + + +App/task/ui_task.c->Drivers/BSP/UI/ui_net_sink.h + + + + + +Drivers/BSP/UI/ui_port.h + +Drivers/BSP/UI/ui_port.h + + + +App/task/ui_task.c->Drivers/BSP/UI/ui_port.h + + + + + +Drivers/BSP/UI/ui_standby.h + +Drivers/BSP/UI/ui_standby.h + + + +App/task/ui_task.c->Drivers/BSP/UI/ui_standby.h + + + + + +App/task/ui_task.c->Drivers/BSP/dbg_log.h + + + + + +App/task/ui_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + + + + + +test/board/net_latency_test.h + +test/board/net_latency_test.h + + + +App/task/ui_task.c->test/board/net_latency_test.h + + + + + +App/task/ui_task.c->test/board/test_config.h + + + + + +App/task/ui_task.c->外部/系统头文件 + + + + + +App/util/sha256.c + +App/util/sha256.c + + + +App/util/sha256.h + +App/util/sha256.h + + + +App/util/sha256.c->App/util/sha256.h + + + + + +App/util/sha256.c->外部/系统头文件 + + + + + +App/util/sha256.h->外部/系统头文件 + + + + + +App/util/sys_clock.c + +App/util/sys_clock.c + + + +App/util/sys_clock.c->App/util/sys_clock.h + + + + + +App/util/sys_clock.c->Drivers/BSP/dbg_log.h + + + + + +App/util/sys_clock.c->Inc/main.h + + + + + +App/util/sys_clock.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h + + + + + +App/util/sys_clock.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h + + + + + +App/util/sys_clock.c->外部/系统头文件 + + + + + +App/util/sys_clock.h->Drivers/BSP/SD2506/sd2506.h + + + + + +App/util/sys_clock.h->外部/系统头文件 + + + + + +App/util/task_prof.c + +App/util/task_prof.c + + + +App/util/task_prof.c->App/log/console_log.h + + + + + +App/util/task_prof.c->App/util/task_prof.h + + + + + +App/util/task_prof.c->Inc/main.h + + + + + +App/util/task_prof.c->外部/系统头文件 + + + + + +App/util/task_prof.h->外部/系统头文件 + + + + + +App/util/task_watchdog.c + +App/util/task_watchdog.c + + + +App/util/task_watchdog.c->App/log/console_log.h + + + + + +App/util/task_watchdog.c->App/log/sys_log.h + + + + + +App/util/task_watchdog.c->App/util/task_watchdog.h + + + + + +Drivers/BSP/NET/lftpd/lftpd.h + +Drivers/BSP/NET/lftpd/lftpd.h + + + +App/util/task_watchdog.c->Drivers/BSP/NET/lftpd/lftpd.h + + + + + +App/util/task_watchdog.c->Drivers/BSP/dbg_log.h + + + + + +Inc/iwdg.h + +Inc/iwdg.h + + + +App/util/task_watchdog.c->Inc/iwdg.h + + + + + +App/util/task_watchdog.c->Inc/main.h + + + + + +App/util/task_watchdog.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h + + + + + +App/util/task_watchdog.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h + + + + + +App/util/task_watchdog.c->外部/系统头文件 + + + + + +App/util/task_watchdog.h->外部/系统头文件 + + + + + +Drivers/BSP/CH395F/ch395f.c + +Drivers/BSP/CH395F/ch395f.c + + + +Drivers/BSP/CH395F/ch395f.c->Drivers/BSP/CH395F/ch395f.h + + + + + +Drivers/BSP/CH395F/ch395f.c->Drivers/BSP/dbg_log.h + + + + + +Inc/spi.h + +Inc/spi.h + + + +Drivers/BSP/CH395F/ch395f.c->Inc/spi.h + + + + + +Drivers/BSP/CH395F/ch395f.c->Inc/usart.h + + + + + +Drivers/BSP/CH395F/ch395f.c->外部/系统头文件 + + + + + +Drivers/BSP/CH395F/ch395f.h->Inc/main.h + + + + + +Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c + +Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c + + + +Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c->Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h + + + + + +Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c->Drivers/BSP/dbg_log.h + + + + + +Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c->外部/系统头文件 + + + + + +Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h->Inc/main.h + + + + + +Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h->外部/系统头文件 + + + + + +Drivers/BSP/GD5F2GQ5UE/nand_ftl.c + +Drivers/BSP/GD5F2GQ5UE/nand_ftl.c + + + +Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h + + + + + +Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Drivers/BSP/dbg_log.h + + + + + +Lib/FatFs/diskio.h + +Lib/FatFs/diskio.h + + + +Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Lib/FatFs/diskio.h + + + + + +Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Lib/FatFs/ff.h + + + + + +Lib/dhara/map.h + +Lib/dhara/map.h + + + +Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Lib/dhara/map.h + + + + + +Lib/dhara/nand.h + +Lib/dhara/nand.h + + + +Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Lib/dhara/nand.h + + + + + +Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->外部/系统头文件 + + + + + +Drivers/BSP/GD5F2GQ5UE/nand_ftl.h->外部/系统头文件 + + + + + +Drivers/BSP/NET/lftpd/lftpd_auth.c + +Drivers/BSP/NET/lftpd/lftpd_auth.c + + + +Drivers/BSP/NET/net_config.h + +Drivers/BSP/NET/net_config.h + + + +Drivers/BSP/NET/net_select.c + +Drivers/BSP/NET/net_select.c + + + +Drivers/BSP/NET/net_select.c->Drivers/BSP/CH395F/ch395f.h + + + + + +Drivers/BSP/NET/net_select.c->Drivers/BSP/NET/net_select.h + + + + + +Drivers/BSP/NET/net_select.c->Drivers/BSP/NET/net_socket.h + + + + + +Drivers/BSP/NET/net_select.c->Inc/main.h + + + + + +Drivers/BSP/NET/net_select.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + + + + + +Drivers/BSP/NET/net_select.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h + + + + + +Drivers/BSP/NET/net_select.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h + + + + + +Drivers/BSP/NET/net_select.c->外部/系统头文件 + + + + + +Drivers/BSP/NET/net_select.h->Drivers/BSP/NET/net_config.h + + + + + +Drivers/BSP/NET/net_types.h + +Drivers/BSP/NET/net_types.h + + + +Drivers/BSP/NET/net_select.h->Drivers/BSP/NET/net_types.h + + + + + +Drivers/BSP/NET/net_socket.c + +Drivers/BSP/NET/net_socket.c + + + +Drivers/BSP/NET/net_socket.c->Drivers/BSP/CH395F/ch395f.h + + + + + +Drivers/BSP/NET/net_socket.c->Drivers/BSP/NET/net_socket.h + + + + + +Drivers/BSP/NET/net_socket.c->Drivers/BSP/dbg_log.h + + + + + +Drivers/BSP/NET/net_socket.c->Inc/main.h + + + + + +Drivers/BSP/NET/net_socket.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + + + + + +Drivers/BSP/NET/net_socket.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h + + + + + +Drivers/BSP/NET/net_socket.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h + + + + + +Drivers/BSP/NET/net_socket.c->test/board/net_latency_test.h + + + + + +Drivers/BSP/NET/net_socket.c->test/board/test_config.h + + + + + +Drivers/BSP/NET/net_socket.c->外部/系统头文件 + + + + + +Drivers/BSP/NET/net_socket.h->Drivers/BSP/NET/net_types.h + + + + + +Drivers/BSP/NET/net_socket.h->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h + + + + + +Drivers/BSP/NET/net_socket.h->Middlewares/Third_Party/FreeRTOS/Source/include/task.h + + + + + +Drivers/BSP/NET/net_types.h->Drivers/BSP/NET/net_config.h + + + + + +Drivers/BSP/NET/net_types.h->外部/系统头文件 + + + + + +Drivers/BSP/NETCFG/ftp_cfg.c + +Drivers/BSP/NETCFG/ftp_cfg.c + + + +Drivers/BSP/NETCFG/ftp_cfg.c->App/cfg/cfg_file.h + + + + + +Drivers/BSP/NETCFG/ftp_cfg.h + +Drivers/BSP/NETCFG/ftp_cfg.h + + + +Drivers/BSP/NETCFG/ftp_cfg.c->Drivers/BSP/NETCFG/ftp_cfg.h + + + + + +Drivers/BSP/NETCFG/ftp_cfg.c->外部/系统头文件 + + + + + +Drivers/BSP/NETCFG/ftp_cfg.h->外部/系统头文件 + + + + + +Drivers/BSP/NETCFG/net_cfg.c + +Drivers/BSP/NETCFG/net_cfg.c + + + +Drivers/BSP/NETCFG/net_cfg.c->App/cfg/cfg_file.h + + + + + +Drivers/BSP/NETCFG/net_cfg.c->Drivers/BSP/NETCFG/ftp_cfg.h + + + + + +Drivers/BSP/NETCFG/net_cfg.c->Drivers/BSP/NETCFG/net_cfg.h + + + + + +Drivers/BSP/NETCFG/net_cfg.c->Drivers/BSP/dbg_log.h + + + + + +Drivers/BSP/NETCFG/net_cfg.c->外部/系统头文件 + + + + + +Drivers/BSP/NETCFG/net_cfg.h->外部/系统头文件 + + + + + +Drivers/BSP/RS485/rs485.c + +Drivers/BSP/RS485/rs485.c + + + +Drivers/BSP/RS485/rs485.c->Drivers/BSP/RS485/rs485.h + + + + + +Drivers/BSP/RS485/rs485.h->Inc/main.h + + + + + +Drivers/BSP/SD2506/sd2506.c + +Drivers/BSP/SD2506/sd2506.c + + + +Drivers/BSP/SD2506/sd2506.c->Drivers/BSP/SD2506/sd2506.h + + + + + +Drivers/BSP/SD2506/sd2506.c->Drivers/BSP/dbg_log.h + + + + + +Inc/i2c.h + +Inc/i2c.h + + + +Drivers/BSP/SD2506/sd2506.c->Inc/i2c.h + + + + + +Drivers/BSP/SD2506/sd2506.c->外部/系统头文件 + + + + + +Drivers/BSP/SD2506/sd2506.h->Inc/main.h + + + + + +Drivers/BSP/TPAFE5160/tpafe5160.c + +Drivers/BSP/TPAFE5160/tpafe5160.c + + + +Drivers/BSP/TPAFE5160/tpafe5160.c->Drivers/BSP/TPAFE5160/tpafe5160.h + + + + + +Drivers/BSP/TPAFE5160/tpafe5160.c->Drivers/BSP/dbg_log.h + + + + + +Drivers/BSP/TPAFE5160/tpafe5160.c->Inc/main.h + + + + + +Drivers/BSP/TPAFE5160/tpafe5160.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h + + + + + +Drivers/BSP/TPAFE5160/tpafe5160.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h + + + + + +Drivers/BSP/TPAFE5160/tpafe5160.c->外部/系统头文件 + + + + + +Drivers/BSP/TPAFE5160/tpafe5160.h->Inc/main.h + + + + + +Drivers/BSP/UI/ui_content_page.c + +Drivers/BSP/UI/ui_content_page.c + + + +Drivers/BSP/UI/ui_content_page.h + +Drivers/BSP/UI/ui_content_page.h + + + +Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_content_page.h + + + + + +Drivers/BSP/UI/ui_deco.h + +Drivers/BSP/UI/ui_deco.h + + + +Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_deco.h + + + + + +Drivers/BSP/UI/ui_menu_view.h + +Drivers/BSP/UI/ui_menu_view.h + + + +Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_menu_view.h + + + + + +Drivers/BSP/UI/ui_page_ac.h + +Drivers/BSP/UI/ui_page_ac.h + + + +Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_ac.h + + + + + +Drivers/BSP/UI/ui_page_dc.h + +Drivers/BSP/UI/ui_page_dc.h + + + +Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_dc.h + + + + + +Drivers/BSP/UI/ui_page_device_info.h + +Drivers/BSP/UI/ui_page_device_info.h + + + +Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_device_info.h + + + + + +Drivers/BSP/UI/ui_page_net_cfg.h + +Drivers/BSP/UI/ui_page_net_cfg.h + + + +Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_net_cfg.h + + + + + +Drivers/BSP/UI/ui_page_password.h + +Drivers/BSP/UI/ui_page_password.h + + + +Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_password.h + + + + + +Drivers/BSP/UI/ui_page_runlog.h + +Drivers/BSP/UI/ui_page_runlog.h + + + +Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_runlog.h + + + + + +Drivers/BSP/UI/ui_page_selftest.h + +Drivers/BSP/UI/ui_page_selftest.h + + + +Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_selftest.h + + + + + +Drivers/BSP/UI/ui_pwd_def.h + +Drivers/BSP/UI/ui_pwd_def.h + + + +Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_pwd_def.h + + + + + +Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_standby.h + + + + + +Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/dbg_log.h + + + + + +Lib/lvgl/lvgl.h + +Lib/lvgl/lvgl.h + + + +Drivers/BSP/UI/ui_content_page.c->Lib/lvgl/lvgl.h + + + + + +Drivers/BSP/UI/ui_content_page.c->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_content_page.h->Lib/lvgl/lvgl.h + + + + + +Drivers/BSP/UI/ui_content_page.h->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_deco.c + +Drivers/BSP/UI/ui_deco.c + + + +Drivers/BSP/UI/ui_deco.c->Drivers/BSP/UI/ui_deco.h + + + + + +Drivers/BSP/UI/ui_deco.c->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_deco.h->Lib/lvgl/lvgl.h + + + + + +Drivers/BSP/UI/ui_font_cn_12.c + +Drivers/BSP/UI/ui_font_cn_12.c + + + +Drivers/BSP/UI/ui_font_cn_12.c->Lib/lvgl/lvgl.h + + + + + +Drivers/BSP/UI/ui_menu.c + +Drivers/BSP/UI/ui_menu.c + + + +Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_content_page.h + + + + + +Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_menu.h + + + + + +Drivers/BSP/UI/ui_menu_tab.h + +Drivers/BSP/UI/ui_menu_tab.h + + + +Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_menu_tab.h + + + + + +Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_menu_view.h + + + + + +Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_page_password.h + + + + + +Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_pwd_def.h + + + + + +Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_standby.h + + + + + +Drivers/BSP/UI/ui_menu.c->Drivers/BSP/dbg_log.h + + + + + +Drivers/BSP/UI/ui_menu.c->Lib/lvgl/lvgl.h + + + + + +Drivers/BSP/UI/ui_menu.c->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_menu_tab.c + +Drivers/BSP/UI/ui_menu_tab.c + + + +Drivers/BSP/UI/ui_menu_tab.c->Drivers/BSP/UI/ui_menu_tab.h + + + + + +Drivers/BSP/UI/ui_menu_tab.h->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_menu_view.c + +Drivers/BSP/UI/ui_menu_view.c + + + +Drivers/BSP/UI/ui_menu_view.c->Drivers/BSP/UI/ui_deco.h + + + + + +Drivers/BSP/UI/ui_menu_view.c->Drivers/BSP/UI/ui_menu_tab.h + + + + + +Drivers/BSP/UI/ui_menu_view.c->Drivers/BSP/UI/ui_menu_view.h + + + + + +Drivers/BSP/UI/ui_menu_view.c->Drivers/BSP/dbg_log.h + + + + + +Drivers/BSP/UI/ui_menu_view.c->Lib/lvgl/lvgl.h + + + + + +Drivers/BSP/UI/ui_menu_view.c->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_menu_view.h->Lib/lvgl/lvgl.h + + + + + +Drivers/BSP/UI/ui_menu_view.h->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_net_sink.c + +Drivers/BSP/UI/ui_net_sink.c + + + +Drivers/BSP/UI/ui_net_sink.c->Drivers/BSP/NET/net_socket.h + + + + + +Drivers/BSP/UI/ui_net_sink.c->Drivers/BSP/NET/net_types.h + + + + + +Drivers/BSP/UI/ui_net_sink.c->Drivers/BSP/UI/ui_net_sink.h + + + + + +Drivers/BSP/UI/ui_net_sink_backend.h + +Drivers/BSP/UI/ui_net_sink_backend.h + + + +Drivers/BSP/UI/ui_net_sink.c->Drivers/BSP/UI/ui_net_sink_backend.h + + + + + +Drivers/BSP/UI/ui_net_sink.c->Drivers/BSP/dbg_log.h + + + + + +Drivers/BSP/UI/ui_net_sink.c->test/board/net_latency_test.h + + + + + +Drivers/BSP/UI/ui_net_sink.c->test/board/test_config.h + + + + + +Drivers/BSP/UI/ui_net_sink.c->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_net_sink.h->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_net_sink_backend.h->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_net_sink_backend_mcu.c + +Drivers/BSP/UI/ui_net_sink_backend_mcu.c + + + +Drivers/BSP/UI/ui_net_sink_backend_mcu.c->Drivers/BSP/UI/ui_net_sink_backend.h + + + + + +Drivers/BSP/UI/ui_net_sink_backend_mcu.c->Drivers/BSP/UI/ui_port.h + + + + + +Drivers/BSP/UI/ui_net_sink_backend_mcu.c->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_page_ac.c + +Drivers/BSP/UI/ui_page_ac.c + + + +Drivers/BSP/UI/ui_page_ac.c->Drivers/BSP/UI/ui_page_ac.h + + + + + +Drivers/BSP/UI/ui_page_ac.h->Drivers/BSP/UI/ui_content_page.h + + + + + +Drivers/BSP/UI/ui_page_dc.c + +Drivers/BSP/UI/ui_page_dc.c + + + +Drivers/BSP/UI/ui_page_dc.c->Drivers/BSP/UI/ui_page_dc.h + + + + + +Drivers/BSP/UI/ui_page_dc.h->Drivers/BSP/UI/ui_content_page.h + + + + + +Drivers/BSP/UI/ui_page_device_info.c + +Drivers/BSP/UI/ui_page_device_info.c + + + +Drivers/BSP/UI/ui_page_device_info.c->Drivers/BSP/UI/ui_page_device_info.h + + + + + +Drivers/BSP/UI/ui_page_device_info.h->Drivers/BSP/UI/ui_content_page.h + + + + + +Drivers/BSP/UI/ui_page_net_cfg.c + +Drivers/BSP/UI/ui_page_net_cfg.c + + + +Drivers/BSP/UI/ui_page_net_cfg.c->Drivers/BSP/NETCFG/net_cfg.h + + + + + +Drivers/BSP/UI/ui_page_net_cfg.c->Drivers/BSP/UI/ui_page_net_cfg.h + + + + + +Drivers/BSP/UI/ui_page_net_cfg.c->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_page_net_cfg.h->Drivers/BSP/UI/ui_content_page.h + + + + + +Drivers/BSP/UI/ui_page_password.c + +Drivers/BSP/UI/ui_page_password.c + + + +Drivers/BSP/UI/ui_page_password.c->Drivers/BSP/UI/ui_page_password.h + + + + + +Drivers/BSP/UI/ui_page_password.c->Drivers/BSP/UI/ui_pwd_def.h + + + + + +Drivers/BSP/UI/ui_page_password.c->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_page_password.h->Drivers/BSP/UI/ui_content_page.h + + + + + +Drivers/BSP/UI/ui_page_runlog.c + +Drivers/BSP/UI/ui_page_runlog.c + + + +Drivers/BSP/UI/ui_page_runlog.c->App/log/sys_log.h + + + + + +Drivers/BSP/UI/ui_page_runlog.c->Drivers/BSP/UI/ui_page_runlog.h + + + + + +Drivers/BSP/UI/ui_page_runlog.c->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_page_runlog.h->Drivers/BSP/UI/ui_content_page.h + + + + + +Drivers/BSP/UI/ui_page_selftest.c + +Drivers/BSP/UI/ui_page_selftest.c + + + +Drivers/BSP/UI/ui_page_selftest.c->App/log/sys_log.h + + + + + +Drivers/BSP/UI/ui_page_selftest.c->Drivers/BSP/UI/ui_page_selftest.h + + + + + +Drivers/BSP/UI/ui_page_selftest.c->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_page_selftest.h->Drivers/BSP/UI/ui_content_page.h + + + + + +Drivers/BSP/UI/ui_port.c + +Drivers/BSP/UI/ui_port.c + + + +Drivers/BSP/UI/ui_port.c->Drivers/BSP/UI/ui_port.h + + + + + +Drivers/BSP/UI/ui_port.c->Drivers/BSP/dbg_log.h + + + + + +Drivers/BSP/UI/ui_port.c->Lib/lvgl/lvgl.h + + + + + +Drivers/BSP/UI/ui_port.c->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_port.h->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_pwd_def.h->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_standby.c + +Drivers/BSP/UI/ui_standby.c + + + +Drivers/BSP/UI/ui_standby.c->Drivers/BSP/UI/ui_menu_view.h + + + + + +Drivers/BSP/UI/ui_standby.c->Drivers/BSP/UI/ui_standby.h + + + + + +Drivers/BSP/UI/ui_standby.c->Drivers/BSP/dbg_log.h + + + + + +Drivers/BSP/UI/ui_standby.c->Lib/lvgl/lvgl.h + + + + + +Drivers/BSP/UI/ui_standby.c->外部/系统头文件 + + + + + +Drivers/BSP/UI/ui_standby.h->外部/系统头文件 + + + + + +Drivers/BSP/dbg_cfg.h + +Drivers/BSP/dbg_cfg.h + + + +Drivers/BSP/dbg_log.h->App/log/console_log.h + + + + + +Drivers/BSP/dbg_log.h->App/util/sys_clock.h + + + + + +Drivers/BSP/dbg_log.h->Drivers/BSP/dbg_cfg.h + + + + + +Drivers/BSP/dbg_log.h->外部/系统头文件 + + + + + +Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h + +Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h + + + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h + + + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc.h + + + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_cortex.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_cortex.h + + + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_dma.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_dma.h + + + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_exti.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_exti.h + + + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_flash.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_flash.h + + + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_gpio.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_gpio.h + + + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_i2c.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_i2c.h + + + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_iwdg.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_iwdg.h + + + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_pwr.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_pwr.h + + + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_rcc.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_rcc.h + + + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_spi.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_spi.h + + + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_tim.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_tim.h + + + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_uart.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_uart.h + + + +Inc/FreeRTOSConfig.h + +Inc/FreeRTOSConfig.h + + + +Inc/FreeRTOSConfig.h->外部/系统头文件 + + - + Inc/adc.h->Inc/main.h - - + + - + Inc/dma.h - -Inc/dma.h + +Inc/dma.h - + Inc/dma.h->Inc/main.h - - + + - + Inc/gpio.h - -Inc/gpio.h + +Inc/gpio.h - + Inc/gpio.h->Inc/main.h - - + + - + Inc/i2c.h->Inc/main.h - - + + - + Inc/iwdg.h->Inc/main.h - - + + - + Inc/main.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h - - + + - + Inc/spi.h->Inc/main.h - - + + - + Inc/stm32f4xx_hal_conf.h - -Inc/stm32f4xx_hal_conf.h + +Inc/stm32f4xx_hal_conf.h - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_cortex.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_dma.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_exti.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_flash.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_gpio.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_i2c.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_iwdg.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_pwr.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_rcc.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_spi.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_tim.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_uart.h - - + + - + Inc/stm32f4xx_hal_conf.h->外部/系统头文件 - - + + + + + +Inc/tim.h->Inc/main.h + + - + Inc/usart.h->Inc/main.h - - + + - + Src/adc.c - -Src/adc.c + +Src/adc.c - + Src/adc.c->Inc/adc.h - - + + - + Src/dma.c - -Src/dma.c + +Src/dma.c - + Src/dma.c->Inc/dma.h - - + + - + Src/freertos.c - -Src/freertos.c + +Src/freertos.c - + Src/freertos.c->App/app/sys_events.h - - + + - + Src/freertos.c->App/sync/time_sync.h - - + + + + + +Src/freertos.c->App/task/adc_task.h + + - + Src/freertos.c->App/task/default_task.h - - + + + + + +Src/freertos.c->App/task/iec104_serv.h + + - + Src/freertos.c->App/task/net_task.h - - + + + + + +Src/freertos.c->App/task/protect_task.h + + - + Src/freertos.c->App/util/task_watchdog.h - - + + - + Src/freertos.c->Drivers/BSP/CH395F/ch395f.h - - + + - + Src/freertos.c->Drivers/BSP/NET/lftpd/lftpd.h - - + + - + Src/freertos.c->Drivers/BSP/NET/net_socket.h - - + + - + Src/freertos.c->Drivers/BSP/dbg_log.h - - + + - + Src/freertos.c->Inc/main.h - - + + - + Src/freertos.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + Src/freertos.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + - + Src/freertos.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - + + - + test/board/ch395f_test.h - -test/board/ch395f_test.h + +test/board/ch395f_test.h - + Src/freertos.c->test/board/ch395f_test.h - - + + - + test/board/ch395f_test_task.h - -test/board/ch395f_test_task.h + +test/board/ch395f_test_task.h - + Src/freertos.c->test/board/ch395f_test_task.h - - + + - + Src/freertos.c->test/board/test_config.h - - + + - + Src/freertos.c->外部/系统头文件 - - + + - + Src/gpio.c - -Src/gpio.c + +Src/gpio.c - + Src/gpio.c->Inc/gpio.h - - + + - + Src/i2c.c - -Src/i2c.c + +Src/i2c.c - + Src/i2c.c->Inc/i2c.h - - + + - + Src/iwdg.c - -Src/iwdg.c + +Src/iwdg.c - + Src/iwdg.c->Inc/iwdg.h - - + + - + Src/main.c - -Src/main.c + +Src/main.c - + Src/main.c->App/app/app_main.h - - + + + + + +Src/main.c->App/task/adc_task.h + + - + Src/main.c->App/util/sys_clock.h - - + + - + Src/main.c->App/util/task_watchdog.h - - + + - + Src/main.c->Drivers/BSP/CH395F/ch395f.h - - + + - + Src/main.c->Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - - + + - + Src/main.c->Drivers/BSP/NET/net_select.h - - + + - + Src/main.c->Drivers/BSP/NET/net_socket.h - - + + - + Src/main.c->Drivers/BSP/RS485/rs485.h - - + + - + Src/main.c->Drivers/BSP/SD2506/sd2506.h - - + + - + Src/main.c->Drivers/BSP/TPAFE5160/tpafe5160.h - - + + - + Src/main.c->Drivers/BSP/dbg_log.h - - + + - + Src/main.c->Inc/adc.h - - + + - + Src/main.c->Inc/dma.h - - + + - + Src/main.c->Inc/gpio.h - - + + - + Src/main.c->Inc/i2c.h - - + + - + Src/main.c->Inc/iwdg.h - - + + - + Src/main.c->Inc/main.h - - + + - + Src/main.c->Inc/spi.h - - + + + + + +Src/main.c->Inc/tim.h + + - + Src/main.c->Inc/usart.h - - + + - + Src/main.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + Src/main.c->外部/系统头文件 - - + + - + Src/spi.c - -Src/spi.c + +Src/spi.c - + Src/spi.c->Inc/spi.h - - + + - + Src/stm32f4xx_hal_msp.c - -Src/stm32f4xx_hal_msp.c + +Src/stm32f4xx_hal_msp.c - + Src/stm32f4xx_hal_msp.c->Inc/main.h - - + + - + Src/stm32f4xx_hal_timebase_tim.c - -Src/stm32f4xx_hal_timebase_tim.c + +Src/stm32f4xx_hal_timebase_tim.c - + Src/stm32f4xx_hal_timebase_tim.c->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h - - + + - + Src/stm32f4xx_hal_timebase_tim.c->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_tim.h - - + + - + Src/stm32f4xx_it.c - -Src/stm32f4xx_it.c + +Src/stm32f4xx_it.c - + Src/stm32f4xx_it.c->Inc/main.h - - + + - + Src/stm32f4xx_it.c->Inc/stm32f4xx_it.h - - + + - + Src/stm32f4xx_it.c->Inc/usart.h - - + + - + Src/system_stm32f4xx.c - -Src/system_stm32f4xx.c + +Src/system_stm32f4xx.c - + Src/system_stm32f4xx.c->Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h - - + + + + + +Src/tim.c + +Src/tim.c + + + +Src/tim.c->Inc/tim.h + + - + Src/usart.c - -Src/usart.c + +Src/usart.c - + Src/usart.c->Inc/usart.h - - + + - + simulator/SDL2/SDL2-2.32.10/include/SDL.h - -simulator/SDL2/SDL2-2.32.10/include/SDL.h + +simulator/SDL2/SDL2-2.32.10/include/SDL.h - + simulator/dbg_log.h - -simulator/dbg_log.h + +simulator/dbg_log.h - + simulator/dbg_log.h->外部/系统头文件 - - + + - + simulator/lv_conf.h - -simulator/lv_conf.h + +simulator/lv_conf.h - + simulator/main.c - -simulator/main.c + +simulator/main.c - + simulator/main.c->App/cfg/cfg_file.h - - + + - + simulator/main.c->App/cfg/cfg_file_store.h - - + + - + simulator/main.c->App/log/sys_log.h - - + + - + simulator/main.c->Drivers/BSP/NETCFG/net_cfg.h - - + + - + simulator/main.c->Drivers/BSP/UI/ui_menu.h - - + + - + simulator/main.c->Drivers/BSP/UI/ui_net_sink.h - - + + - + simulator/main.c->Drivers/BSP/UI/ui_standby.h - - + + - + simulator/main.c->Lib/lvgl/lvgl.h - - + + - + simulator/main.c->simulator/SDL2/SDL2-2.32.10/include/SDL.h - - + + - + simulator/sim/sim_port.h - -simulator/sim/sim_port.h + +simulator/sim/sim_port.h - + simulator/main.c->simulator/sim/sim_port.h - - + + - + simulator/main.c->外部/系统头文件 - - + + - + simulator/sim/cfg_file_store_sim.c - -simulator/sim/cfg_file_store_sim.c + +simulator/sim/cfg_file_store_sim.c - + simulator/sim/cfg_file_store_sim.c->App/cfg/cfg_file_store.h - - + + - + simulator/sim/cfg_file_store_sim.c->外部/系统头文件 - - + + - + simulator/sim/net_socket_sim.c - -simulator/sim/net_socket_sim.c + +simulator/sim/net_socket_sim.c - + simulator/sim/net_socket_sim.c->Drivers/BSP/NET/net_socket.h - - + + - + simulator/sim/net_socket_sim.c->外部/系统头文件 - - + + - + simulator/sim/sim_latency_stub.c - -simulator/sim/sim_latency_stub.c + +simulator/sim/sim_latency_stub.c - + simulator/sim/sim_latency_stub.c->test/board/net_latency_test.h - - + + - + simulator/sim/sim_port.c - -simulator/sim/sim_port.c + +simulator/sim/sim_port.c - + simulator/sim/sim_port.c->Lib/lvgl/lvgl.h - - + + - + simulator/sim/sim_port.c->simulator/SDL2/SDL2-2.32.10/include/SDL.h - - + + - + simulator/sim/sim_port.c->simulator/sim/sim_port.h - - + + - + simulator/sim/sim_port.h->外部/系统头文件 - - + + - + simulator/sim/sim_syslog.c - -simulator/sim/sim_syslog.c + +simulator/sim/sim_syslog.c - + simulator/sim/sim_syslog.c->App/log/sys_log.h - - + + - + simulator/sim/sim_syslog.c->外部/系统头文件 - - + + - + simulator/sim/ui_net_sink_backend_sim.c - -simulator/sim/ui_net_sink_backend_sim.c + +simulator/sim/ui_net_sink_backend_sim.c - + simulator/sim/ui_net_sink_backend_sim.c->Drivers/BSP/UI/ui_net_sink_backend.h - - + + - + simulator/sim/ui_net_sink_backend_sim.c->Drivers/BSP/UI/ui_port.h - - + + - + simulator/sim/ui_net_sink_backend_sim.c->Lib/lvgl/lvgl.h - - + + - + simulator/sim/ui_net_sink_backend_sim.c->simulator/sim/sim_port.h - - + + + + + +test/board/ch395f_reuse_bench.c + +test/board/ch395f_reuse_bench.c + + + +test/board/ch395f_reuse_bench.c->Drivers/BSP/CH395F/ch395f.h + + + + + +test/board/ch395f_reuse_bench.c->Drivers/BSP/dbg_log.h + + + + + +test/board/ch395f_reuse_bench.c->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h + + + + + +test/board/ch395f_reuse_bench.h + +test/board/ch395f_reuse_bench.h + + + +test/board/ch395f_reuse_bench.c->test/board/ch395f_reuse_bench.h + + - + test/board/test_common.h - -test/board/test_common.h + +test/board/test_common.h - + test/board/ch395f_test.h->test/board/test_common.h - - + + - + test/board/ch395f_test.h->外部/系统头文件 - - + + - + test/board/ch395f_test_task.c - -test/board/ch395f_test_task.c + +test/board/ch395f_test_task.c - + test/board/ch395f_test_task.c->App/app/sys_events.h - - + + - + test/board/ch395f_test_task.c->App/task/net_task.h - - + + - + test/board/ch395f_test_task.c->Drivers/BSP/CH395F/ch395f.h - - + + - + test/board/ch395f_test_task.c->Drivers/BSP/NET/net_socket.h - - + + - + test/board/ch395f_test_task.c->Drivers/BSP/dbg_log.h - - + + - + test/board/ch395f_test_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + test/board/ch395f_test_task.c->test/board/ch395f_test.h - - + + - + test/board/ch395f_test_task.c->test/board/ch395f_test_task.h - - + + - + test/board/gd5f_test.h - -test/board/gd5f_test.h + +test/board/gd5f_test.h - + test/board/ch395f_test_task.c->test/board/gd5f_test.h - - + + - + test/board/ch395f_test_task.c->test/board/net_latency_test.h - - + + - + test/board/net_test_task.h - -test/board/net_test_task.h + +test/board/net_test_task.h - + test/board/ch395f_test_task.c->test/board/net_test_task.h - - + + - + test/board/sd2506_test.h - -test/board/sd2506_test.h + +test/board/sd2506_test.h - + test/board/ch395f_test_task.c->test/board/sd2506_test.h - - + + - + test/board/storage_test.h - -test/board/storage_test.h + +test/board/storage_test.h - + test/board/ch395f_test_task.c->test/board/storage_test.h - - + + - + test/board/ch395f_test_task.c->test/board/test_config.h - - + + - + test/board/ch395f_test_task.c->外部/系统头文件 - - + + - + test/board/gd5f_test.h->Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - - + + - + test/board/gd5f_test.h->test/board/test_common.h - - + + - + test/board/gd5f_test.h->外部/系统头文件 - - + + - + test/board/gd5f_test_task.c - -test/board/gd5f_test_task.c + +test/board/gd5f_test_task.c - + test/board/gd5f_test_task.c->Drivers/BSP/GD5F2GQ5UE/nand_ftl.h - - + + - + test/board/gd5f_test_task.c->Drivers/BSP/dbg_log.h - - + + - + test/board/gd5f_test_task.c->Lib/FatFs/ff.h - - + + - + test/board/gd5f_test_task.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + - + test/board/gd5f_test_task.c->test/board/gd5f_test.h - - + + - + test/board/gd5f_test_task.c->test/board/test_config.h - - + + - + test/board/gd5f_test_task.c->外部/系统头文件 - - + + - + test/board/net_latency_test.c - -test/board/net_latency_test.c + +test/board/net_latency_test.c - + test/board/net_latency_test.c->Drivers/BSP/CH395F/ch395f.h - - + + - + test/board/net_latency_test.c->Drivers/BSP/NET/net_socket.h - - + + - + test/board/net_latency_test.c->Drivers/BSP/dbg_log.h - - + + - + test/board/net_latency_test.c->Inc/main.h - - + + - + test/board/net_latency_test.c->test/board/ch395f_test.h - - + + - + test/board/net_latency_test.c->test/board/net_latency_test.h - - + + - + test/board/net_latency_test.c->test/board/test_config.h - - + + - + test/board/net_latency_test.c->外部/系统头文件 - - + + - + test/board/net_latency_test.h->外部/系统头文件 - - + + - + test/board/net_test_task.c - -test/board/net_test_task.c + +test/board/net_test_task.c - + test/board/net_test_task.c->Drivers/BSP/NET/net_select.h - - + + - + test/board/net_test_task.c->Drivers/BSP/NET/net_socket.h - - + + - + test/board/net_test_task.c->Drivers/BSP/NET/net_types.h - - + + - + test/board/net_test_task.c->Drivers/BSP/dbg_log.h - - + + - + test/board/net_test_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + test/board/net_test_task.c->test/board/ch395f_test.h - - + + - + test/board/net_test_task.c->test/board/ch395f_test_task.h - - + + - + test/board/net_test_task.c->test/board/net_latency_test.h - - + + - + test/board/net_test_task.c->test/board/net_test_task.h - - + + - + test/board/net_test_task.c->外部/系统头文件 - - + + - + test/board/sd2506_test.h->test/board/test_common.h - - + + - + test/board/sd2506_test.h->外部/系统头文件 - - + + - + test/board/sd2506_test_task.c - -test/board/sd2506_test_task.c + +test/board/sd2506_test_task.c - + test/board/sd2506_test_task.c->Drivers/BSP/SD2506/sd2506.h - - + + - + test/board/sd2506_test_task.c->Drivers/BSP/dbg_log.h - - + + - + test/board/sd2506_test_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + test/board/sd2506_test_task.c->test/board/sd2506_test.h - - + + - + test/board/sd2506_test_task.c->test/board/test_config.h - - + + - + test/board/sd2506_test_task.c->外部/系统头文件 - - + + - + test/board/storage_test.h->test/board/test_common.h - - + + - + test/board/storage_test.h->外部/系统头文件 - - + + - + test/board/storage_test_task.c - -test/board/storage_test_task.c + +test/board/storage_test_task.c - + test/board/storage_test_task.c->Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - - + + - + test/board/storage_test_task.c->Drivers/BSP/GD5F2GQ5UE/nand_ftl.h - - + + - + test/board/storage_test_task.c->Drivers/BSP/dbg_log.h - - + + - + test/board/storage_test_task.c->Inc/main.h - - + + - + test/board/storage_test_task.c->Lib/FatFs/diskio.h - - + + - + test/board/storage_test_task.c->Lib/FatFs/ff.h - - + + - + test/board/storage_test_task.c->test/board/storage_test.h - - + + - + test/board/storage_test_task.c->test/board/test_config.h - - + + - + test/board/storage_test_task.c->外部/系统头文件 - - + + - + test/board/test_common.h->外部/系统头文件 - - + + - + test/board/test_config.c - -test/board/test_config.c + +test/board/test_config.c - + test/board/test_config.c->Drivers/BSP/dbg_log.h - - + + - + test/board/test_config.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + test/board/test_config.c->test/board/ch395f_test_task.h - - + + - + test/board/test_config.c->test/board/gd5f_test.h - - + + - + test/board/test_config.c->test/board/sd2506_test.h - - + + - + test/board/test_config.c->test/board/storage_test.h - - + + - + test/board/test_config.c->test/board/test_config.h - - + + - + test/board/test_config.h->test/board/ch395f_test.h - - + + - + test/board/test_config.h->test/board/gd5f_test.h - - + + - + test/board/test_config.h->test/board/sd2506_test.h - - + + - + test/board/test_config.h->test/board/storage_test.h - - + + - + test/board/test_config.h->外部/系统头文件 - - + + + + + +test/host/adc_health/adc_health_test.c + +test/host/adc_health/adc_health_test.c + + + +test/host/adc_health/adc_health_test.c->App/algo/adc_health.c + + + + + +test/host/adc_health/adc_health_test.c->外部/系统头文件 + + + + + +test/host/adc_input/adc_input_test.c + +test/host/adc_input/adc_input_test.c + + + +test/host/adc_input/adc_input_test.c->App/algo/adc_input.c + + + + + +test/host/adc_input/adc_input_test.c->App/algo/adc_input.h + + + + + +test/host/adc_input/adc_input_test.c->Drivers/BSP/TPAFE5160/tpafe5160.h + + + + + +test/host/adc_input/adc_input_test.c->Inc/adc.h + + + + + +test/host/adc_input/adc_input_test.c->外部/系统头文件 + + + + + +test/host/adc_input/stub/adc.h + +test/host/adc_input/stub/adc.h + + + +test/host/adc_input/stub/adc.h->外部/系统头文件 + + + + + +test/host/adc_input/stub/tpafe5160.h + +test/host/adc_input/stub/tpafe5160.h + + + +test/host/adc_input/stub/tpafe5160.h->外部/系统头文件 + + - + test/host/cfg_file/cfg_file_test.c - -test/host/cfg_file/cfg_file_test.c + +test/host/cfg_file/cfg_file_test.c - + test/host/cfg_file/cfg_file_test.c->App/cfg/cfg_file.c - - + + - + test/host/cfg_file/cfg_file_test.c->App/cfg/cfg_file_store.h - - + + - + test/host/cfg_file/cfg_file_test.c->App/log/sys_log.h - - + + - + test/host/cfg_file/cfg_file_test.c->Drivers/BSP/dbg_log.h - - + + - + test/host/cfg_file/cfg_file_test.c->外部/系统头文件 - - + + - + test/host/cfg_file/stub/dbg_log.h - -test/host/cfg_file/stub/dbg_log.h + +test/host/cfg_file/stub/dbg_log.h - + test/host/cfg_file/stub/dbg_log.h->外部/系统头文件 - - + + + + + +test/host/ftp_auth/ftp_auth_test.c + +test/host/ftp_auth/ftp_auth_test.c + + + +test/host/ftp_auth/ftp_auth_test.c->App/util/sha256.c + + + + + +test/host/ftp_auth/ftp_auth_test.c->Drivers/BSP/NET/lftpd/lftpd_auth.c + + + + + +test/host/ftp_auth/ftp_auth_test.c->外部/系统头文件 + + + + + +test/host/iec104/iec104_test.c + +test/host/iec104/iec104_test.c + + + +test/host/iec104/iec104_test.c->App/algo/phasor.c + + + + + +test/host/iec104/iec104_test.c->App/algo/prot_elm.c + + + + + +test/host/iec104/iec104_test.c->App/algo/prot_set.c + + + + + +test/host/iec104/iec104_test.c->App/algo/sample_ring.c + + + + + +test/host/iec104/iec104_test.c->App/protocol/iec104/iec104_asdu.c + + + + + +test/host/iec104/iec104_test.c->App/protocol/iec104/iec104_link.c + + + + + +test/host/iec104/iec104_test.c->App/protocol/iec104/iec104_pts.c + + + + + +test/host/iec104/iec104_test.c->外部/系统头文件 + + + + + +test/host/phasor/phasor_test.c + +test/host/phasor/phasor_test.c + + + +test/host/phasor/phasor_test.c->App/algo/phasor.c + + + + + +test/host/phasor/phasor_test.c->App/algo/sample_ring.c + + + + + +test/host/phasor/phasor_test.c->外部/系统头文件 + + + + + +test/host/prot_elm/prot_elm_test.c + +test/host/prot_elm/prot_elm_test.c + + + +test/host/prot_elm/prot_elm_test.c->App/algo/phasor.c + + + + + +test/host/prot_elm/prot_elm_test.c->App/algo/prot_elm.c + + + + + +test/host/prot_elm/prot_elm_test.c->App/algo/prot_set.c + + + + + +test/host/prot_elm/prot_elm_test.c->App/algo/sample_ring.c + + + + + +test/host/prot_elm/prot_elm_test.c->外部/系统头文件 + + + + + +test/host/prot_set/prot_set_test.c + +test/host/prot_set/prot_set_test.c + + + +test/host/prot_set/prot_set_test.c->App/algo/prot_set.c + + + + + +test/host/prot_set/prot_set_test.c->外部/系统头文件 + + + + + +test/host/sha256/sha256_test.c + +test/host/sha256/sha256_test.c + + + +test/host/sha256/sha256_test.c->App/util/sha256.c + + + + + +test/host/sha256/sha256_test.c->外部/系统头文件 + + - + test/host/sys_clock/stub/FreeRTOS.h - -test/host/sys_clock/stub/FreeRTOS.h + +test/host/sys_clock/stub/FreeRTOS.h - + test/host/sys_clock/stub/FreeRTOS.h->外部/系统头文件 - - + + - + test/host/sys_clock/stub/dbg_log.h - -test/host/sys_clock/stub/dbg_log.h + +test/host/sys_clock/stub/dbg_log.h - + test/host/sys_clock/stub/dbg_log.h->外部/系统头文件 - - + + - + test/host/sys_clock/stub/main.h - -test/host/sys_clock/stub/main.h + +test/host/sys_clock/stub/main.h - + test/host/sys_clock/stub/main.h->外部/系统头文件 - - + + - + test/host/sys_clock/stub/task.h - -test/host/sys_clock/stub/task.h + +test/host/sys_clock/stub/task.h - + test/host/sys_clock/stub/task.h->test/host/sys_clock/stub/FreeRTOS.h - - + + - + test/host/sys_clock/sys_clock_test.c - -test/host/sys_clock/sys_clock_test.c + +test/host/sys_clock/sys_clock_test.c - + test/host/sys_clock/sys_clock_test.c->App/util/sys_clock.c - - + + - + test/host/sys_clock/sys_clock_test.c->App/util/sys_clock.h - - + + - + test/host/sys_clock/sys_clock_test.c->Drivers/BSP/SD2506/sd2506.h - - + + - + test/host/sys_clock/sys_clock_test.c->Drivers/BSP/dbg_log.h - - + + - + test/host/sys_clock/sys_clock_test.c->Inc/main.h - - + + - + test/host/sys_clock/sys_clock_test.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + - + test/host/sys_clock/sys_clock_test.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - + + - + test/host/sys_clock/sys_clock_test.c->外部/系统头文件 - - + + + + + +test/host/time_sync/stub/FreeRTOS.h + +test/host/time_sync/stub/FreeRTOS.h + + + +test/host/time_sync/stub/FreeRTOS.h->外部/系统头文件 + + + + + +test/host/time_sync/stub/cmsis_os2.h + +test/host/time_sync/stub/cmsis_os2.h + + + +test/host/time_sync/stub/cmsis_os2.h->外部/系统头文件 + + + + + +test/host/time_sync/stub/dbg_log.h + +test/host/time_sync/stub/dbg_log.h + + + +test/host/time_sync/stub/dbg_log.h->外部/系统头文件 + + + + + +test/host/time_sync/stub/net_socket.h + +test/host/time_sync/stub/net_socket.h + + + +test/host/time_sync/stub/net_socket.h->test/host/time_sync/stub/FreeRTOS.h + + + + + +test/host/time_sync/stub/task.h + +test/host/time_sync/stub/task.h + + + +test/host/time_sync/stub/net_socket.h->test/host/time_sync/stub/task.h + + + + + +test/host/time_sync/stub/net_socket.h->外部/系统头文件 + + + + + +test/host/time_sync/stub/sys_clock.h + +test/host/time_sync/stub/sys_clock.h + + + +test/host/time_sync/stub/sys_clock.h->外部/系统头文件 + + + + + +test/host/time_sync/stub/sys_events.h + +test/host/time_sync/stub/sys_events.h + + + +test/host/time_sync/stub/sys_events.h->test/host/time_sync/stub/cmsis_os2.h + + + + + +test/host/time_sync/stub/task.h->test/host/time_sync/stub/FreeRTOS.h + + + + + +test/host/time_sync/time_sync_test.c + +test/host/time_sync/time_sync_test.c + + + +test/host/time_sync/time_sync_test.c->App/app/sys_events.h + + + + + +test/host/time_sync/time_sync_test.c->App/sync/time_sync.c + + + + + +test/host/time_sync/time_sync_test.c->App/util/sys_clock.h + + + + + +test/host/time_sync/time_sync_test.c->App/util/task_watchdog.h + + + + + +test/host/time_sync/time_sync_test.c->Drivers/BSP/NET/net_socket.h + + + + + +test/host/time_sync/time_sync_test.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h + + + + + +test/host/time_sync/time_sync_test.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h + + + + + +test/host/time_sync/time_sync_test.c->外部/系统头文件 + + + + + +test/host/vol_lock/stub/cmsis_os.h + +test/host/vol_lock/stub/cmsis_os.h + + + +test/host/vol_lock/stub/cmsis_os.h->外部/系统头文件 + + + + + +test/host/vol_lock/vol_lock_test.c + +test/host/vol_lock/vol_lock_test.c + + + +test/host/vol_lock/vol_lock_test.c->App/fs/vol_lock.c + + + + + +test/host/vol_lock/vol_lock_test.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + + + + + +test/host/vol_lock/vol_lock_test.c->外部/系统头文件 + + - + test/host/watchdog/stub/dbg_log.h - -test/host/watchdog/stub/dbg_log.h + +test/host/watchdog/stub/dbg_log.h - + test/host/watchdog/stub/dbg_log.h->外部/系统头文件 - - + + - + test/host/watchdog/stub/iwdg.h - -test/host/watchdog/stub/iwdg.h + +test/host/watchdog/stub/iwdg.h - + test/host/watchdog/stub/iwdg.h->外部/系统头文件 - - + + - + test/host/watchdog/stub/main.h - -test/host/watchdog/stub/main.h + +test/host/watchdog/stub/main.h - + test/host/watchdog/stub/main.h->外部/系统头文件 - - + + - + test/host/watchdog/watchdog_test.c - -test/host/watchdog/watchdog_test.c + +test/host/watchdog/watchdog_test.c - + test/host/watchdog/watchdog_test.c->App/util/task_watchdog.c - - + + - + test/host/watchdog/watchdog_test.c->Drivers/BSP/dbg_log.h - - + + - + test/host/watchdog/watchdog_test.c->Inc/iwdg.h - - + + - + test/host/watchdog/watchdog_test.c->Inc/main.h - - + + - + test/host/watchdog/watchdog_test.c->外部/系统头文件 - - + + diff --git a/docs/images/引用关系图_模块.dot b/docs/images/引用关系图_模块.dot index 61e6c8c6..e8df23f9 100644 --- a/docs/images/引用关系图_模块.dot +++ b/docs/images/引用关系图_模块.dot @@ -35,11 +35,12 @@ digraph refgraph_module { "App" -> "BSP/NET" [color="#2563eb", penwidth=1.2, label="×1"]; "App" -> "BSP/RS485" [color="#2563eb", penwidth=1.2, label="×1"]; "App" -> "BSP/SD2506" [color="#2563eb", penwidth=1.2, label="×1"]; - "App" -> "BSP/TPAFE5160" [color="#2563eb", penwidth=1.2, label="×1"]; + "App" -> "BSP/TPAFE5160" [color="#2563eb", penwidth=1.2, label="×2"]; "App" -> "FatFs" [color="#2563eb", penwidth=1.2, label="×1"]; "App" -> "FreeRTOS" [color="#2563eb", penwidth=1.2, label="×3"]; - "App" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×3"]; - "App" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×8"]; + "App" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×5"]; + "App" -> "test" [color="#2563eb", penwidth=1.2, label="×1"]; + "App" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×24"]; "App/log" -> "App" [color="#2563eb", penwidth=1.2, label="×2"]; "App/log" -> "App/util" [color="#2563eb", penwidth=1.2, label="×1"]; "App/log" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×1"]; @@ -47,29 +48,29 @@ digraph refgraph_module { "App/log" -> "FreeRTOS" [color="#2563eb", penwidth=1.2, label="×4"]; "App/log" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×1"]; "App/log" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×4"]; - "App/task" -> "App" [color="#2563eb", penwidth=1.2, label="×5"]; - "App/task" -> "App/log" [color="#2563eb", penwidth=1.2, label="×2"]; - "App/task" -> "App/util" [color="#2563eb", penwidth=1.2, label="×4"]; - "App/task" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×5"]; + "App/task" -> "App" [color="#2563eb", penwidth=1.2, label="×15"]; + "App/task" -> "App/log" [color="#2563eb", penwidth=1.2, label="×4"]; + "App/task" -> "App/util" [color="#2563eb", penwidth=1.2, label="×12"]; + "App/task" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×7"]; "App/task" -> "BSP/CH395F" [color="#2563eb", penwidth=1.2, label="×1"]; "App/task" -> "BSP/GD5F2GQ5UE" [color="#2563eb", penwidth=1.2, label="×1"]; - "App/task" -> "BSP/NET" [color="#2563eb", penwidth=1.2, label="×2"]; + "App/task" -> "BSP/NET" [color="#2563eb", penwidth=1.2, label="×3"]; "App/task" -> "BSP/NETCFG" [color="#2563eb", penwidth=1.2, label="×2"]; "App/task" -> "BSP/RS485" [color="#2563eb", penwidth=1.2, label="×1"]; "App/task" -> "BSP/TPAFE5160" [color="#2563eb", penwidth=1.2, label="×1"]; "App/task" -> "BSP/UI" [color="#2563eb", penwidth=1.2, label="×4"]; "App/task" -> "FatFs" [color="#2563eb", penwidth=1.2, label="×1"]; - "App/task" -> "FreeRTOS" [color="#2563eb", penwidth=1.2, label="×7"]; + "App/task" -> "FreeRTOS" [color="#2563eb", penwidth=1.2, label="×9"]; "App/task" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×1"]; "App/task" -> "test" [color="#2563eb", penwidth=1.2, label="×4"]; - "App/task" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×2"]; - "App/util" -> "App/log" [color="#2563eb", penwidth=1.2, label="×2"]; + "App/task" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×3"]; + "App/util" -> "App/log" [color="#2563eb", penwidth=1.2, label="×3"]; "App/util" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×2"]; "App/util" -> "BSP/SD2506" [color="#2563eb", penwidth=1.2, label="×1"]; "App/util" -> "FreeRTOS" [color="#2563eb", penwidth=1.2, label="×4"]; - "App/util" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×3"]; + "App/util" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×4"]; "App/util" -> "lftpd" [color="#2563eb", penwidth=1.2, label="×1"]; - "App/util" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×4"]; + "App/util" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×8"]; "BSP(公共)" -> "App/log" [color="#2563eb", penwidth=1.2, label="×1"]; "BSP(公共)" -> "App/util" [color="#2563eb", penwidth=1.2, label="×1"]; "BSP(公共)" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×1"]; @@ -87,9 +88,9 @@ digraph refgraph_module { "BSP/NET" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×2"]; "BSP/NET" -> "test" [color="#2563eb", penwidth=1.2, label="×2"]; "BSP/NET" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×3"]; - "BSP/NETCFG" -> "App" [color="#2563eb", penwidth=1.2, label="×1"]; + "BSP/NETCFG" -> "App" [color="#2563eb", penwidth=1.2, label="×2"]; "BSP/NETCFG" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×1"]; - "BSP/NETCFG" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×2"]; + "BSP/NETCFG" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×4"]; "BSP/RS485" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×1"]; "BSP/SD2506" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×1"]; "BSP/SD2506" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×2"]; @@ -114,7 +115,7 @@ digraph refgraph_module { "FreeRTOS" -> "CMSIS" [color="#94a3b8", penwidth=0.7, label="×1"]; "FreeRTOS" -> "Src/Inc (CubeMX)" [color="#94a3b8", penwidth=0.7, label="×1"]; "FreeRTOS" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×14"]; - "HAL" -> "CMSIS" [color="#94a3b8", penwidth=0.7, label="×15"]; + "HAL" -> "CMSIS" [color="#94a3b8", penwidth=0.7, label="×16"]; "HAL" -> "Src/Inc (CubeMX)" [color="#94a3b8", penwidth=0.7, label="×1"]; "HAL" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×1"]; "LVGL" -> "FatFs" [color="#94a3b8", penwidth=0.7, label="×1"]; @@ -124,7 +125,7 @@ digraph refgraph_module { "LVGL" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×204"]; "SDL2" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×36"]; "Src/Inc (CubeMX)" -> "App" [color="#2563eb", penwidth=1.2, label="×3"]; - "Src/Inc (CubeMX)" -> "App/task" [color="#2563eb", penwidth=1.2, label="×2"]; + "Src/Inc (CubeMX)" -> "App/task" [color="#2563eb", penwidth=1.2, label="×6"]; "Src/Inc (CubeMX)" -> "App/util" [color="#2563eb", penwidth=1.2, label="×3"]; "Src/Inc (CubeMX)" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×2"]; "Src/Inc (CubeMX)" -> "BSP/CH395F" [color="#2563eb", penwidth=1.2, label="×2"]; @@ -141,11 +142,13 @@ digraph refgraph_module { "Src/Inc (CubeMX)" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×4"]; "dhara" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×5"]; "lftpd" -> "App" [color="#94a3b8", penwidth=0.7, label="×1"]; - "lftpd" -> "BSP(公共)" [color="#94a3b8", penwidth=0.7, label="×4"]; + "lftpd" -> "App/util" [color="#94a3b8", penwidth=0.7, label="×1"]; + "lftpd" -> "BSP(公共)" [color="#94a3b8", penwidth=0.7, label="×3"]; "lftpd" -> "BSP/NET" [color="#94a3b8", penwidth=0.7, label="×3"]; + "lftpd" -> "BSP/NETCFG" [color="#94a3b8", penwidth=0.7, label="×1"]; "lftpd" -> "FatFs" [color="#94a3b8", penwidth=0.7, label="×2"]; "lftpd" -> "FreeRTOS" [color="#94a3b8", penwidth=0.7, label="×2"]; - "lftpd" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×8"]; + "lftpd" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×9"]; "simulator" -> "App" [color="#2563eb", penwidth=1.2, label="×3"]; "simulator" -> "App/log" [color="#2563eb", penwidth=1.2, label="×2"]; "simulator" -> "BSP/NET" [color="#2563eb", penwidth=1.2, label="×1"]; @@ -155,17 +158,20 @@ digraph refgraph_module { "simulator" -> "SDL2" [color="#2563eb", penwidth=1.2, label="×2"]; "simulator" -> "test" [color="#2563eb", penwidth=1.2, label="×1"]; "simulator" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×6"]; - "test" -> "App" [color="#2563eb", penwidth=1.2, label="×3"]; + "test" -> "App" [color="#2563eb", penwidth=1.2, label="×23"]; "test" -> "App/log" [color="#2563eb", penwidth=1.2, label="×1"]; "test" -> "App/task" [color="#2563eb", penwidth=1.2, label="×1"]; - "test" -> "App/util" [color="#2563eb", penwidth=1.2, label="×3"]; - "test" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×10"]; - "test" -> "BSP/CH395F" [color="#2563eb", penwidth=1.2, label="×2"]; + "test" -> "App/util" [color="#2563eb", penwidth=1.2, label="×7"]; + "test" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×11"]; + "test" -> "BSP/CH395F" [color="#2563eb", penwidth=1.2, label="×3"]; "test" -> "BSP/GD5F2GQ5UE" [color="#2563eb", penwidth=1.2, label="×4"]; - "test" -> "BSP/NET" [color="#2563eb", penwidth=1.2, label="×5"]; + "test" -> "BSP/NET" [color="#2563eb", penwidth=1.2, label="×6"]; "test" -> "BSP/SD2506" [color="#2563eb", penwidth=1.2, label="×2"]; + "test" -> "BSP/TPAFE5160" [color="#2563eb", penwidth=1.2, label="×1"]; "test" -> "FatFs" [color="#2563eb", penwidth=1.2, label="×3"]; - "test" -> "FreeRTOS" [color="#2563eb", penwidth=1.2, label="×7"]; - "test" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×5"]; - "test" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×23"]; + "test" -> "FreeRTOS" [color="#2563eb", penwidth=1.2, label="×10"]; + "test" -> "HAL" [color="#2563eb", penwidth=1.2, label="×1"]; + "test" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×6"]; + "test" -> "lftpd" [color="#2563eb", penwidth=1.2, label="×1"]; + "test" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×41"]; } diff --git a/docs/images/引用关系图_模块.svg b/docs/images/引用关系图_模块.svg index d29713cb..f0b36343 100644 --- a/docs/images/引用关系图_模块.svg +++ b/docs/images/引用关系图_模块.svg @@ -4,1147 +4,1189 @@ - - + + refgraph_module - -STM32F4-Base 模块引用概览 + +STM32F4-Base 模块引用概览 App - -App + +App App/log - -App/log + +App/log App->App/log - - -×2 + + +×2 App/util - -App/util + +App/util App->App/util - - -×4 + + +×4 BSP(公共) - -BSP(公共) + +BSP(公共) App->BSP(公共) - - -×3 + + +×3 BSP/GD5F2GQ5UE - -BSP/GD5F2GQ5UE + +BSP/GD5F2GQ5UE App->BSP/GD5F2GQ5UE - - -×1 + + +×1 BSP/NET - -BSP/NET + +BSP/NET App->BSP/NET - - -×1 + + +×1 BSP/RS485 - -BSP/RS485 + +BSP/RS485 App->BSP/RS485 - - -×1 + + +×1 BSP/SD2506 - -BSP/SD2506 + +BSP/SD2506 App->BSP/SD2506 - - -×1 + + +×1 BSP/TPAFE5160 - -BSP/TPAFE5160 + +BSP/TPAFE5160 App->BSP/TPAFE5160 - - -×1 + + +×2 FatFs - -FatFs + +FatFs App->FatFs - - -×1 + + +×1 FreeRTOS - -FreeRTOS + +FreeRTOS App->FreeRTOS - - -×3 + + +×3 Src/Inc (CubeMX) - -Src/Inc (CubeMX) + +Src/Inc (CubeMX) App->Src/Inc (CubeMX) - - -×3 - - - -外部/系统头文件 - -外部/系统头文件 - - - -App->外部/系统头文件 - - -×8 - - - -App/log->App - - -×2 - - - -App/log->App/util - - -×1 - - - -App/log->BSP(公共) - - -×1 - - - -App/log->FatFs - - -×1 - - - -App/log->FreeRTOS - - -×4 - - - -App/log->Src/Inc (CubeMX) - - -×1 - - - -App/log->外部/系统头文件 - - -×4 - - - -App/task - -App/task - - - -App/task->App - - -×5 - - - -App/task->App/log - - -×2 - - - -App/task->App/util - - -×4 - - - -App/task->BSP(公共) - - -×5 - - - -BSP/CH395F - -BSP/CH395F - - - -App/task->BSP/CH395F - - -×1 - - - -App/task->BSP/GD5F2GQ5UE - - -×1 - - - -App/task->BSP/NET - - -×2 - - - -BSP/NETCFG - -BSP/NETCFG - - - -App/task->BSP/NETCFG - - -×2 - - - -App/task->BSP/RS485 - - -×1 - - - -App/task->BSP/TPAFE5160 - - -×1 - - - -BSP/UI - -BSP/UI - - - -App/task->BSP/UI - - -×4 - - - -App/task->FatFs - - -×1 - - - -App/task->FreeRTOS - - -×7 - - - -App/task->Src/Inc (CubeMX) - - -×1 + + +×5 test - -test + +test + + + +App->test + + +×1 + + + +外部/系统头文件 + +外部/系统头文件 + + + +App->外部/系统头文件 + + +×24 + + + +App/log->App + + +×2 + + + +App/log->App/util + + +×1 + + + +App/log->BSP(公共) + + +×1 + + + +App/log->FatFs + + +×1 + + + +App/log->FreeRTOS + + +×4 + + + +App/log->Src/Inc (CubeMX) + + +×1 + + + +App/log->外部/系统头文件 + + +×4 + + + +App/task + +App/task + + + +App/task->App + + +×15 + + + +App/task->App/log + + +×4 + + + +App/task->App/util + + +×12 + + + +App/task->BSP(公共) + + +×7 + + + +BSP/CH395F + +BSP/CH395F + + + +App/task->BSP/CH395F + + +×1 + + + +App/task->BSP/GD5F2GQ5UE + + +×1 + + + +App/task->BSP/NET + + +×3 + + + +BSP/NETCFG + +BSP/NETCFG + + + +App/task->BSP/NETCFG + + +×2 + + + +App/task->BSP/RS485 + + +×1 + + + +App/task->BSP/TPAFE5160 + + +×1 + + + +BSP/UI + +BSP/UI + + + +App/task->BSP/UI + + +×4 + + + +App/task->FatFs + + +×1 + + + +App/task->FreeRTOS + + +×9 + + + +App/task->Src/Inc (CubeMX) + + +×1 - + App/task->test - - -×4 + + +×4 - + App/task->外部/系统头文件 - - -×2 + + +×3 - + App/util->App/log - - -×2 + + +×3 - + App/util->BSP(公共) - - -×2 + + +×2 - + App/util->BSP/SD2506 - - -×1 + + +×1 - + App/util->FreeRTOS - - -×4 + + +×4 - + App/util->Src/Inc (CubeMX) - - -×3 + + +×4 lftpd - -lftpd + +lftpd - + App/util->lftpd - - -×1 + + +×1 - + App/util->外部/系统头文件 - - -×4 + + +×8 - + BSP(公共)->App/log - - -×1 + + +×1 - + BSP(公共)->App/util - - -×1 + + +×1 - + BSP(公共)->外部/系统头文件 - - -×1 + + +×1 - + BSP/CH395F->BSP(公共) - - -×1 + + +×1 - + BSP/CH395F->Src/Inc (CubeMX) - - -×3 + + +×3 - + BSP/CH395F->外部/系统头文件 - - -×1 + + +×1 - + BSP/GD5F2GQ5UE->BSP(公共) - - -×2 + + +×2 - + BSP/GD5F2GQ5UE->FatFs - - -×2 + + +×2 - + BSP/GD5F2GQ5UE->Src/Inc (CubeMX) - - -×1 + + +×1 dhara - -dhara + +dhara - + BSP/GD5F2GQ5UE->dhara - - -×2 + + +×2 - + BSP/GD5F2GQ5UE->外部/系统头文件 - - -×4 + + +×4 - + BSP/NET->BSP(公共) - - -×1 + + +×1 - + BSP/NET->BSP/CH395F - - -×2 + + +×2 - + BSP/NET->FreeRTOS - - -×8 + + +×8 - + BSP/NET->Src/Inc (CubeMX) - - -×2 + + +×2 - + BSP/NET->test - - -×2 + + +×2 - + BSP/NET->外部/系统头文件 - - -×3 + + +×3 - + BSP/NETCFG->App - - -×1 + + +×2 - + BSP/NETCFG->BSP(公共) - - -×1 + + +×1 - + BSP/NETCFG->外部/系统头文件 - - -×2 + + +×4 - + BSP/RS485->Src/Inc (CubeMX) - - -×1 + + +×1 - + BSP/SD2506->BSP(公共) - - -×1 + + +×1 - + BSP/SD2506->Src/Inc (CubeMX) - - -×2 + + +×2 - + BSP/SD2506->外部/系统头文件 - - -×1 + + +×1 - + BSP/TPAFE5160->BSP(公共) - - -×1 + + +×1 - + BSP/TPAFE5160->FreeRTOS - - -×2 + + +×2 - + BSP/TPAFE5160->Src/Inc (CubeMX) - - -×2 + + +×2 - + BSP/TPAFE5160->外部/系统头文件 - - -×1 + + +×1 - + BSP/UI->App/log - - -×2 + + +×2 - + BSP/UI->BSP(公共) - - -×6 + + +×6 - + BSP/UI->BSP/NET - - -×2 + + +×2 - + BSP/UI->BSP/NETCFG - - -×1 + + +×1 LVGL - -LVGL + +LVGL - + BSP/UI->LVGL - - -×9 + + +×9 - + BSP/UI->test - - -×2 + + +×2 - + BSP/UI->外部/系统头文件 - - -×20 + + +×20 CMSIS - -CMSIS + +CMSIS HAL - -HAL + +HAL - + CMSIS->HAL - - -×1 + + +×1 - + CMSIS->外部/系统头文件 - - -×22 + + +×22 - + FatFs->App/util - - -×1 + + +×1 - + FatFs->BSP(公共) - - -×1 + + +×1 - + FatFs->FreeRTOS - - -×1 + + +×1 - + FatFs->外部/系统头文件 - - -×2 + + +×2 - + FreeRTOS->CMSIS - - -×1 + + +×1 - + FreeRTOS->Src/Inc (CubeMX) - - -×1 + + +×1 - + FreeRTOS->外部/系统头文件 - - -×14 + + +×14 - + HAL->CMSIS - - -×15 + + +×16 - + HAL->Src/Inc (CubeMX) - - -×1 + + +×1 - + HAL->外部/系统头文件 - - -×1 + + +×1 - + LVGL->FatFs - - -×1 + + +×1 - + LVGL->FreeRTOS - - -×6 + + +×6 SDL2 - -SDL2 + +SDL2 - + LVGL->SDL2 - - -×1 + + +×1 其他(第三方) - -其他(第三方) + +其他(第三方) - + LVGL->其他(第三方) - - -×1 + + +×1 - + LVGL->外部/系统头文件 - - -×204 + + +×204 - + SDL2->外部/系统头文件 - - -×36 + + +×36 - + Src/Inc (CubeMX)->App - - -×3 + + +×3 - + Src/Inc (CubeMX)->App/task - - -×2 + + +×6 - + Src/Inc (CubeMX)->App/util - - -×3 + + +×3 - + Src/Inc (CubeMX)->BSP(公共) - - -×2 + + +×2 - + Src/Inc (CubeMX)->BSP/CH395F - - -×2 + + +×2 - + Src/Inc (CubeMX)->BSP/GD5F2GQ5UE - - -×1 + + +×1 - + Src/Inc (CubeMX)->BSP/NET - - -×3 + + +×3 - + Src/Inc (CubeMX)->BSP/RS485 - - -×1 + + +×1 - + Src/Inc (CubeMX)->BSP/SD2506 - - -×1 + + +×1 - + Src/Inc (CubeMX)->BSP/TPAFE5160 - - -×1 + + +×1 - + Src/Inc (CubeMX)->CMSIS - - -×1 + + +×1 - + Src/Inc (CubeMX)->FreeRTOS - - -×4 + + +×4 - + Src/Inc (CubeMX)->HAL - - -×16 + + +×16 - + Src/Inc (CubeMX)->lftpd - - -×1 + + +×1 - + Src/Inc (CubeMX)->test - - -×3 + + +×3 - + Src/Inc (CubeMX)->外部/系统头文件 - - -×4 + + +×4 - + dhara->外部/系统头文件 - - -×5 + + +×5 - + lftpd->App - - -×1 + + +×1 + + + +lftpd->App/util + + +×1 - + lftpd->BSP(公共) - - -×4 + + +×3 - + lftpd->BSP/NET - - -×3 + + +×3 + + + +lftpd->BSP/NETCFG + + +×1 - + lftpd->FatFs - - -×2 + + +×2 - + lftpd->FreeRTOS - - -×2 + + +×2 - + lftpd->外部/系统头文件 - - -×8 + + +×9 simulator - -simulator + +simulator - + simulator->App - - -×3 + + +×3 - + simulator->App/log - - -×2 + + +×2 - + simulator->BSP/NET - - -×1 + + +×1 - + simulator->BSP/NETCFG - - -×1 + + +×1 - + simulator->BSP/UI - - -×5 + + +×5 - + simulator->LVGL - - -×3 + + +×3 - + simulator->SDL2 - - -×2 + + +×2 - + simulator->test - - -×1 + + +×1 - + simulator->外部/系统头文件 - - -×6 + + +×6 - + test->App - - -×3 + + +×23 - + test->App/log - - -×1 + + +×1 - + test->App/task - - -×1 + + +×1 - + test->App/util - - -×3 + + +×7 - + test->BSP(公共) - - -×10 + + +×11 - + test->BSP/CH395F - - -×2 + + +×3 - + test->BSP/GD5F2GQ5UE - - -×4 + + +×4 - + test->BSP/NET - - -×5 + + +×6 - + test->BSP/SD2506 - - -×2 + + +×2 + + + +test->BSP/TPAFE5160 + + +×1 - + test->FatFs - - -×3 + + +×3 - + test->FreeRTOS - - -×7 + + +×10 + + + +test->HAL + + +×1 - + test->Src/Inc (CubeMX) - - -×5 + + +×6 + + + +test->lftpd + + +×1 - + test->外部/系统头文件 - - -×23 + + +×41 diff --git a/docs/产品需求/产品需求设计文档.md b/docs/产品需求/产品需求设计文档.md index 35818f54..d63abc67 100644 --- a/docs/产品需求/产品需求设计文档.md +++ b/docs/产品需求/产品需求设计文档.md @@ -24,7 +24,7 @@ 3. **产品当前做到哪一步**——已实现、已验收、待实现的**明确边界**。 **不含**:具体代码实现细节(见各模块设计文档)、测试用例步骤(见 `docs/测试说明/`)、 -未闭环事项的逐条跟踪(见 [`docs/待处理内容.md`](../../docs/待处理内容.md))。 +未闭环事项的逐条跟踪(见 [`docs/任务规划/README.md`](../任务规划/README.md))。 ### 1.2 术语与缩写 @@ -315,7 +315,7 @@ sequenceDiagram - 判据:三个 TCP 服务(FTP / 对时 / 远程显示)均经同一 API 工作;单连接模型下连接断开自动回到 LISTEN。 **FR-NET-3 单连接模型与自愈** -- 需求:CH395F 配置为多 Socket + TCP Server 单连接(`set_fun_para(0x09)`),Socket 0 复用监听与数据通道;网络重建后服务自动重连。 +- 需求:CH395F 配置为 8 Socket(索引寻址)+ TCP Server 单连接(`set_fun_para(0x08)`,bit3 SOCKET_CLOSE;bit0 为保留位必须为 0),Socket 0 复用监听与数据通道;网络重建后服务自动重连。 - 实现:三个 TCP 服务**串行**占用连接,断开自动重听;代次号(`net_get_generation()`)驱动服务自愈。 - 判据:FTP 旧会话异常断开后约 30 s 恢复监听;网络重建(改 IP)后服务自动重建监听。 @@ -465,9 +465,10 @@ sequenceDiagram | 服务 | 协议 / 端口 | 用途 | 核对方法 | |---|---|---|---| -| FTP | TCP **21** | 文件上传/下载(根目录 `/`,NAND 存储) | 任意 FTP 客户端列目录/读写 | +| FTP | TCP **21** | 文件上传/下载(根目录 `/`,NAND 存储) | 登录后任意 FTP 客户端列目录/读写(鉴权见 §7.6) | | 墙钟对时 | TCP **8888** | PC 主动推送时间到设备 | 对时后核对设备时间 / 日志时间戳 | | 远程显示(RemoDispBus) | TCP **7003** | 远端看图(160×160 1bpp)+ 远端导航按键 | DTU-RemoteLCD 或任意 TCP 客户端连 7003 收帧 | +| IEC 60870-5-104 上行 | TCP **2404** | 被控站接入:遥测/遥信上送、总召、遥控/定值下发(B4) | 任意 104 主站连 2404,STARTDT+总召往返(→ `docs/子系统设计/IEC104上行设计说明.md`) | > **现场组网要求**:设备与客户端**同网段**(默认 .100/24、网关 .1);跨网段先改 IP 再上线。 @@ -512,6 +513,7 @@ sequenceDiagram - **实现**:lftpd(轻量 FTP 服务器),root = `/`(FatFS 卷根),端口 21,**单客户端**。 - **支持命令**:登录 / TYPE I / PASV / STOR / RETR / LIST / DELE / QUIT。 +- **鉴权**(已鉴权 · 2026-10-11,N-19):口令登录(`[ftp]` 段 `user` / `pwd_hash` = SHA-256(user‖password) / `write`);未登录仅 USER/PASS/QUIT/FEAT/SYST/NOOP 放行,其余一律 530;`write=0` 可禁 STOR/DELE;连续 5 次失败锁 60 s;出厂默认 `admin`/`admin`,默认口令在用有启动告警。口径详见 `docs/子系统设计/网络配置说明.md` §3.4。 - **启动门**:`ftpTask` 必须等 `NET_READY` **且** `STORAGE_READY`(两位 `NoClear`)才 `lftpd_start`。 - **注意**:同一时刻只服务 1 个会话,旧会话异常断开后约 30 s 恢复监听;测试脚本须内置连接重试,**不要用裸 TCP 工具探测 21 端口**。 - **实测**:128 KB 上传下载逐字节一致(约 49/60 KB/s)。 @@ -678,7 +680,7 @@ cfg-file-unit-test、test-suite-switch、gitlint。 | 项 | 现状 | |---|---| -| FTP 鉴权 | **无**(维持现状,非本期目标) | +| FTP 鉴权 | **已鉴权**(2026-10-11,N-19):口令登录(`[ftp]` 段存 SHA-256 摘要,不存明文)+ 连续 5 次失败锁 60 s + `write=0` 可选只读;出厂默认 `admin`/`admin`,默认口令在用有启动告警。边界:FTP 明文传口令,挡未授权访问、不挡网络嗅探(详见 `docs/子系统设计/网络配置说明.md` §3.4) | | UI 菜单权限 | 受保护叶子(行尾 `#P`)先经密码页;密码**仅编译期写入**(查看 `000` / 修改 `123`),不落 Flash、无运行期设置页 | | 远程显示 | 无鉴权,单连接;现场同网段部署 | @@ -778,24 +780,11 @@ cfg-file-unit-test、test-suite-switch、gitlint。 ### 9.2 阶段路线图 -```text -【阶段 B 功能】 B0 产品定义确认 ✅ ─┬─ B3 E-59 门禁固化(并行) -(当前最高优先) └─ E-58 原子快照 → B1 ADC ✅ → B2 RS485 - ↓ -【阶段 C 结构】 C1 pending 二期 ─ C3 日志管线 ─ C4 栈(S-1) ─ C5 收口 ─ C6 USART1 TX-only - ↓ -【阶段 D 常态】 D2 N-3 L5 基线重锁 / D5 N-16 负向台架 / D6 N-17 探针窗口 -``` - -| 阶段 | 关键任务 | 前置 | -|---|---|---| -| **B0** | 产品定义确认 ✅(2026-10-10):持续采集 / 1000 Hz·20 点周波 / 上行 IEC 60870-5-104(2404)/ RS-485 对下 ModBus RTU;详见 `docs/算法/继电保护功能复现规划.md` §1.5 | — | -| **B1** | ADC 采集链路实现(周期 CONVST + `adc_task` 消费;**先修原子快照 API**) | ✅ 已实现并 `@test` 闭环(2026-10-10) | -| **B2** | RS-485 采集链路实现(双缓冲 + 序号 + DE 收尾 + 协议状态机) | B0 + E-59 门禁 | -| **B3** | E-59 门禁固化入 `AGENTS.md`(接消费者前先修缺陷 + 栈预算) | — | -| **C1** | pending 二期(ACCEPT + SEND pending 化) | — | -| **C4** | `defaultTask` 栈余量(**当前 10%,需尽快处理**):4096→4608 或 GC 链局部削减 | — | -| **C6** | USART1 TX-only 结构根治(灭 ORE 僵尸机制) | 单开一轮 | +> **顺序唯一权威 → [`../任务规划/README.md`](../任务规划/README.md) §2(统一推进顺序,波次 W1~W7; +> RS-485/B2 整簇暂缓见其 §2.4,2026-10-12 用户决定)**。 +> 本节自 2026-10-11 起不再维护路线图(原阶段图删除,避免与登记簿漂移;原 C4"defaultTask 栈余量 10%" +> 已于 2026-10-10 削链闭环销项,见 `../内存管理/RAM使用分析与优化方案.md`)。 +> 功能缺口口径见 §9.1;B0 产品定义结论见 `../算法/继电保护功能复现规划.md` §1.5。 ### 9.3 未接线开关说明 @@ -874,7 +863,7 @@ cfg-file-unit-test、test-suite-switch、gitlint。 | 远程显示协议 | [`docs/UI设计/RemoDispBus协议与接收设计说明.md`](../../docs/UI设计/RemoDispBus协议与接收设计说明.md) | | 部署环境 | [`docs/环境与部署/项目部署环境要求.md`](../../docs/环境与部署/项目部署环境要求.md) | | 硬件网表 | [`docs/硬件平台/V0.1_网表详细分析报告.md`](../../docs/硬件平台/V0.1_网表详细分析报告.md) | -| 待办登记簿 | [`docs/待处理内容.md`](../../docs/待处理内容.md) | +| 待办登记簿 | [`docs/任务规划/README.md`](../任务规划/README.md) | | 体系层评审报告 | [`docs/问题记录/项目审查与优化建议报告.md`](../../docs/问题记录/项目审查与优化建议报告.md) | | 错误码审计 | [`docs/问题记录/错误码传播审计.md`](../../docs/问题记录/错误码传播审计.md) | | 实机测试指南 | [`docs/测试说明/实机测试实施指南.md`](../../docs/测试说明/实机测试实施指南.md) | diff --git a/docs/代码规范/pre-commit使用说明.md b/docs/代码规范/pre-commit使用说明.md index a009a8e7..af3a4732 100644 --- a/docs/代码规范/pre-commit使用说明.md +++ b/docs/代码规范/pre-commit使用说明.md @@ -21,9 +21,12 @@ 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 测试套件开关=产品构建(防测试固件静默提交) + ├─ test-suite-switch 测试套件开关=产品构建(防测试固件静默提交) + ├─ registration-check 双端注册反向校验(uvprojx + CMakeLists,N-23) + └─ mem-budget RAM 预算 92% 阈值门禁(N-27 三件套,2026-10-12) └─ commit-msg(.git/hooks/commit-msg) └─ gitlint 校验 commit message 格式 ``` @@ -47,6 +50,8 @@ git commit | scripts/check_mem_numbers.py | 随仓库提交 | 内存/Flash 数字一致性(纯 Python,无依赖) | | scripts/check_cubemx_survival.py | 随仓库提交 | CubeMX 重生成存活审计(纯 Python,无依赖) | | scripts/check_test_suite.py | 随仓库提交 | 测试套件开关门禁(纯 Python,无依赖) | +| scripts/check_registration.py | 随仓库提交 | 双端注册反向校验(纯 Python,无依赖) | +| scripts/ram_baseline.py | 随仓库提交 | RAM 预算 92% 门禁 + 趋势台账(纯 Python,无依赖) | > **PATH 要求**:cppcheck 安装后需在 PATH 中可找到(winget 自动写入 User PATH,但当前终端不会立即生效,需**重开终端**);装在非 PATH 位置时可用 `DTU_CPPCHECK` 环境变量指向 `cppcheck.exe` 完整路径(显式设置但无效 → 硬报错,不静默回退)。 @@ -162,7 +167,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 带 `/`、任意形态带 `..` / `\` / 绝对路径或盘符;`` 这类**尖括号系统头允许带子目录**,由 `-I` 解析。原实现把任何含 `/` 的 include 一律判违规,误伤模拟器侧 2 处;全仓库复核:细化后仅这 2 处项目代码判定发生变化,其余 100 处均在已排除的 `Lib/lvgl/`) | | S12 | 7.6 | 禁止可变参数函数(`...`) | | S13 | 7.7 | 禁止递归函数 | | S15 | 6.1.4 | 文件作用域指针变量必须初始化为 NULL | @@ -188,7 +193,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 # 全库扫描(开发时自检) @@ -312,11 +318,83 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法 | 项 | 说明 | |---|---| -| **触发条件** | commit message 标题超 100 字符 / 含 body-is-missing 警告 | +| **触发条件** | commit message 标题超 100 字符 / **正文单行超 80 字符** / 其他格式规则 | | **处理方式** | **阻止提交**(commit-msg 阶段) | -| **当前规则** | 宽松:不强制 Conventional Commits,兼容中文标题 | -| **典型报错** | `T1 Title exceeds max length (159>100)` | -| **修复方式** | 精简 commit message 标题到 100 字符以内,或拆分为多行(标题+空行+body) | +| **当前规则** | 宽松:不强制 Conventional Commits,兼容中文标题。配置源是仓库根 `.gitlint`(**不是** hook args):`ignore=body-is-missing`(**不强制写正文**)+ `[title-max-length] line-length=100`;正文行宽走 gitlint 默认 **80**(未关) | +| **典型报错** | `T1 Title exceeds max length (159>100)`、`B4 Line too long (87>80)` | +| **修复方式** | 标题压到 100 字符内(中文长标题极易超限);长说明一律用**短标题 + `git commit -F <文件>` 多行正文**,正文每行 ≤80 字符。消息文件必须 **UTF-8 无 BOM**——用 `-F 文件` 而非 `-m "…"`,防终端代码页转码破坏中文 | + +### 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 字节空对象),价值在防漂移与可读性 | + +### 2.16 sha256-unit-test — SHA-256 主机侧单测(2026-10-11 加,N-19) + +| 项 | 说明 | +|---|---| +| **触发条件** | `App/util/sha256.*`、`test/host/sha256/**` 或 `test/host/common/unit_test_harness.py` 变动 | +| **处理方式** | **阻止提交**(单测失败即拦截) | +| **被测对象** | `App/util/sha256.c/h`(FIPS 180-4 实现,零堆分配)——FTP `pwd_hash` 校验的地基 | +| **用例** | 13 例:FIPS 180-4 标准向量(空串 / `abc` / 56 B 补位跨界 / 1 MB)+ `hex_eq` 非法输入 | +| **为什么需要** | 哈希算错是"正确口令被拒 / 错误口令放行"的**静默语义错误**,板端只见 `530`/`230` 看不出根因;主机侧毫秒级钉死。零平台依赖,无桩 | +| **运行器** | `test/host/sha256/run_unit_test.py`(MSVC `cl.exe`,L0.5) | + +### 2.17 ftp-auth-unit-test — FTP 鉴权状态机主机侧单测(2026-10-11 加,N-19) + +| 项 | 说明 | +|---|---| +| **触发条件** | `Drivers/BSP/NET/lftpd/lftpd_auth.*`、`App/util/sha256.*`、`test/host/ftp_auth/**` 或 `test/host/common/unit_test_harness.py` 变动 | +| **处理方式** | **阻止提交**(单测失败即拦截) | +| **被测对象** | `Drivers/BSP/NET/lftpd/lftpd_auth.c/h`(纯函数鉴权状态机,tick 由调用方参数注入,零平台依赖) | +| **用例** | 36 例:连续 5 败锁定 / 60 s 解锁 / 成功清零 / tick 32 位回绕 | +| **为什么需要** | 锁定 60 s / 解锁 / 清零 / 回绕是**防口令爆破契约核心**——板端每验一条要真等 60 s、回绕要跑满 49.7 天;纯函数设计(tick 注入)就是为让这些分支主机侧毫秒级穷举 | +| **运行器** | `test/host/ftp_auth/run_unit_test.py`(MSVC `cl.exe`,L0.5) | + +### 2.18 registration-check — 双端注册反向校验(2026-10-12 加,N-23) + +| 项 | 说明 | +|---|---| +| **触发条件** | 任意 `.c` 文件 + `*.uvprojx` + `CMakeLists.txt` + 脚本自身变动(pre-commit 4.x 无 `always: true`,`files:` 必须覆盖全部触发面) | +| **处理方式** | **阻止提交**(存在**新**缺口或幽灵注册即拦截);存量缺口走白名单只 `[WARN]` 不拦 | +| **检查对象** | 扫描 `App/`、`Drivers/BSP/`、`Src/` 下全部 `.c`(排除第三方),对照两份注册表:`MDK-ARM/STM32F407-Demo.uvprojx` 的 `` + `simulator/CMakeLists.txt` 的显式源列表(先展开 `${NETCFG_DIR}` 等变量) | +| **三类缺口** | ① 存在但未进 uvprojx(固件不编译)② 存在但未进 CMakeLists(模拟器不编译,**仅当本应进模拟器时**才算)③ 注册了但文件不存在(**幽灵注册**,改名/删除后残留) | +| **豁免机制** | MCU-only 文件不进模拟器是**正确**的(模拟器另有 `*_sim.c` 替身或无对应)。豁免用**显式清单 + 每行理由**(命名模式 `*_mcu.c` + 个别点名),**不整目录一刀切**(N-23 教训正是目录级盲区);唯一目录级豁免 `Src/`(CubeMX 生成,MCU 专属)→ 见脚本 `SIM_EXEMPT` | +| **存量白名单** | `KNOWN_GAPS` 登记 N-23 存量缺口(`App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c`,Lead 处置中)→ 命中打 `[WARN]` 不拦,只对新缺口 FAIL,故钩子可立即上线不拦现有提交;`--strict` 关白名单供审计 | +| **为什么需要** | 新 `.c` 必须手动注册两处才会被编译;历史上 `rs485_task.c`/`lftpd_log.c` **两处都没注册** = 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效(出处审查报告 N-06 残余) | +| **运行器** | `scripts/check_registration.py`(纯标准库;退出码 0=一致 / 1=有缺口 / 2=环境错误) | + +### 2.19 mem-budget — RAM 预算 92% 阈值门禁(2026-10-12 加,N-27 三件套) + +| 项 | 说明 | +|---|---| +| **触发条件** | 全部自研代码目录(`App/` `Drivers/BSP/` `Src/` `Inc/`)+ `Lib/lv_conf.h` + `test/board/` + `scripts/ram_baseline.*` + `docs/内存管理/` + 本配置文件 | +| **处理方式** | **阻止提交**(RW_IRAM1 / RW_CCM 任一区占比 >92%,或台账受管区与 `.map` 漂移);无 `.map` 自适应跳过(同 `mem-numbers` 在 CI 的行为,盲区登记 `docs/任务规划/测试与架构专项.md` A-5 残余) | +| **检查逻辑** | 解析 `.map` 执行区占比 → 超阈值即 FAIL 并附 **Top-10 RW+ZI 对象表**(定位增量元凶);同时校验 `docs/内存管理/RAM预算台账.md` 受管区与 `.map` 一致(漂移 → 提示 `--write`) | +| **软提醒**(不拦) | tsv 末行落后于 `.map`(忘跑 `--update`);`.map` 早于最新源码(忘 `@build`) | +| **配套三件套** | `scripts/ram_baseline.py`(门禁/趋势/受管区生成)+ `scripts/ram_baseline.tsv`(每次 `@build` 后 `--update` 追加的趋势基线,`--trend` 高亮 RW+ZI 增长 >1 KB 的对象)+ `docs/内存管理/RAM预算台账.md`(子系统配额 + CCM 台账 + 超配额流程) | +| **为什么需要** | `docs/问题记录/项目审查与优化建议报告.md` §5.5 第 1 条"超阈值即失败 + Top-10 附表"此前**无任何机械检查**——`mem-numbers` 只防"文档数字漂移"不防"占用超阈值";B4/B5 要新建任务与缓冲,红线必须先机械化(登记簿 N-27 前半 / W1-0) | +| **运行器** | `scripts/ram_baseline.py --gate`(纯标准库,复用 `ram_report.py` 的 `.map` 解析口径;退出码 0=通过/跳过 / 1=超阈值或漂移 / 2=解析失败) | + +### 2.20 iec104-unit-test — IEC 60870-5-104 协议栈主机侧单测(2026-10-12 加,B4) + +| 项 | 说明 | +|---|---| +| **触发条件** | `App/protocol/iec104/iec104_(asdu|pts|link).*`、数据源依赖 `App/algo/(prot_set|prot_elm|phasor|sample_ring|adc_input).*`、`test/host/iec104/**` 或 `test/host/common/unit_test_harness.py` 变动 | +| **处理方式** | **阻止提交**(单测失败即拦截) | +| **被测对象** | `App/protocol/iec104/` 三模块(ASDU 编解码 / 点表+数据源 vtable / APCI+链路 FSM)——纯逻辑、tick 注入、发送经回调注入,零 I/O(A-4 约束 1) | +| **用例** | 82 例:CP56 往返 / APCI 半帧粘帧坏帧 / STARTDT-STOPDT / 总召分片+RBT / COV 死区 / k=12 窗满 / w=8 触发 S / t1-t2-t3 / N(R) 倒退判故障 / 遥控两段式 / 定值越界 PN / C_CS 对时 / 未知类型-地址错误应答 / 周期上送 | +| **为什么需要** | 板端复现这些分支要真主站 + 真等待(t1=15 s、k 窗满要发满 12 帧);link 层 tick 注入 + pts vtable 抽象就是为 L0.5 毫秒级穷举设计;数据源依赖进触发面——测试直接 `#include` 这些生产源,签名变了立刻暴露 | +| **运行器** | `test/host/iec104/run_unit_test.py`(MSVC `cl.exe`,L0.5) | --- @@ -425,13 +503,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** 判违规,误伤 `` 这类系统头;已按 include 形态细化,判定见 §2.8。 + +**教训**:`exclude` 只写**真正不该进钩子的路径**。已被 `.gitignore` 覆盖的生成物不需要再挡一遍——写上去只会连带豁免同一目录里的自研代码。 + --- ## 6. 文件清单 | 文件 | 位置 | 说明 | |---|---|---| -| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(15 个钩子 + 1 个预留) | +| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**31 个钩子**,2026-10-12 清点;本文 §2 详解非单测类——N-19 新增的两个单测钩子见 §2.16/§2.17,N-23 双端注册反向校验见 §2.18,N-27 RAM 预算门禁见 §2.19,B4 IEC104 协议栈单测见 §2.20,其余主机侧单测见 AGENTS.md 门禁条;主机侧单测共 13 个) | | `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) | | `.cubemx_survival_baseline.json` | 仓库根 | CubeMX 易失定制基线(哈希;`--write` 重建) | | `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 | @@ -440,6 +532,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 钩子详解(规则/配置/抑制) | diff --git a/docs/任务规划/README.md b/docs/任务规划/README.md new file mode 100644 index 00000000..5191e452 --- /dev/null +++ b/docs/任务规划/README.md @@ -0,0 +1,137 @@ +# 任务规划(统一待办登记簿 · 索引与排序权威) + +> **这个文件夹是什么**:全项目唯一的未闭环事项清单 + 全项目唯一权威的工作先后顺序。 +> 2026-10-12 按「变更频率 + 职责」解耦成多文件——并行任务写范围互不相交,各改各的文件。 +> +> **文件地图(谁改哪个)**: +> +> | 文件 | 内容 | 变更频率 / 写者 | +> |---|---|---| +> | `README.md`(本文件) | 编号空间(§0)· 未闭环一览(§1)· **推进顺序权威(§2)** · 维护约定(§3) | 低频;仅主工作树 | +> | `W1-采集保护收尾.md` | 当前波次:里程碑 / 并行道 / 任务卡(DoD·依赖·资源·写范围·状态) | **高频**——状态流转只改它;Lead 在 claim/完成时更新 | +> | `排期详述.md` | W4 结构债 / W5 验证常态 波内详表 | 低频 | +> | `暂缓-RS485.md` | B2 RS-485 整簇冻结清单(用户决定) | 极少 | +> | `登记簿.md` | N-x / S-x / T-x 长期登记(网络 / 存储 / 硬件 / 测试 / 报告并轨) | 中频——新发现进对应表 | +> | `测试与架构专项.md` | P1-10 收尾 + S1x / A-x 专项 | 低频 | +> | `基线与待确认.md` | 待确认 · 设计基线(pending 二期 / 错误码四要素)· 诊断基线 · 内存预算 | 参考,少改 | +> +> **怎么用**:想知道"现在该做什么、按什么顺序" → 本文件 **§2**;想知道"当前波次任务卡状态 / 验收判据 / 并行道" → `W1-采集保护收尾.md`; +> 想知道"某条待办的来龙去脉" → 按编号查 `登记簿.md` / `测试与架构专项.md`;想知道"还差哪些验证" → `排期详述.md` §2。 +> +> **只保留待办**:已完成条目直接删除,不留变更记录(历史见 `git log` 与各模块文档); +> 已闭环项的**结论与证据沉淀进对应模块文档/问题记录**,本文件夹不再重复。 +> **消化即销项**:事项修复/关闭后,直接删除对应条目,并同步清理其它文档对本编号的引用。 +> +> **不在本文件夹范围**:`../问题记录/错误码传播审计.md`(E-xx,审计报告,非待办池)、 +> `../问题记录/项目审查与优化建议报告.md`(体系层评审报告)、`../测试说明/实机测试实施指南.md`(执行流程)。 +> +> **修订**:2026-10-04 四档合并;2026-10-11 统一排序 + 审查报告并轨; +> **2026-10-12 按用户决定 RS-485(B2)整簇暂缓,波次重排为 W1~W7,清除已销项残留**; +> **2026-10-12 W1-0 台账三件套落地(N-27 前半闭环,`mem-budget` 钩子),N-23 钩子落地残余并入 C5**; +> **2026-10-12 B4/B5 销项——W1 功能主线代码侧全部完成**; +> **2026-10-12 用户决定:新 W1 = ADC 采集 + 保护相关功能收尾闭环(B6~B10 + N-33),W2~W7 顺延**, +> 波次切换为「里程碑 × 并行道 × 任务卡(五字段)」模式(方法论 → `../环境与部署/并行任务工作树方案.md` §7.3); +> **2026-10-12 解耦为 `docs/任务规划/` 文件夹**(README 索引 + 6 个职责单一文件;编号空间与 §2 排序权威不变)。 + +--- + +## 0. 编号空间与来源 + +| 编号 | 含义 | 备注 | +|---|---|---| +| `P0-x`~`P3-x` | 审计缺陷/改进项(2026-09-15/09-21 审计) | P0=功能 bug、P1=疑似 bug/健壮性、P2=改进、P3=卫生 | +| `N-x` / `S-x` / `T-x` | 台架/审计观察登记项 / 存储·日志管线 / 测试与工具链 | 编号**不回收**;`N-x` 含硬件改版项(需下一版硬件,固件侧只记口径,如 N-18) | +| `S1~S14` / `A-1~A-7` | 2026-10-02 复审的测试充分性与架构专项 | 编号**不回收** | +| `E-xx` | 错误码传播审计发现 | 独立维护于 `错误码传播审计.md`,**不并入本文件夹**,只交叉引用 | +| `B`/`C`/`D` 前缀 | §2 排序条目编号:B=功能主线、C=结构债、D=验证窗口 | 编号不回收,见 §2 | +| 报告 `N-x`/`R-x`/`F-x`/`S-x`/`U-x`/`D-x` | `项目审查与优化建议报告.md` **自有编号** | ⚠ **与本登记簿同名不同义**(报告 N-16 = 日志逐条开关,登记簿 N-16 = A2/A4 负向台架)→ **引用一律带文档名**。其未闭环项 2026-10-11 并入本登记簿(新编 `N-19`~`N-29` + U 簇,出处列保留报告编号) | + +**严重度定义**(沿用审计口径):P0 = 确认功能 bug|P1 = 疑似 bug 或健壮性风险|P2 = 改进|P3 = 卫生。 +**置信度**:high = 代码级确证|medium = 依赖外部条件(手册/硬件/产品定义)。 + +--- + +## 1. 未闭环一览(按类别) + +| 类别 | 数量 | 条目 | +|---|---|---| +| P1 收尾 | 1 | P1-10 测试固件构建实测(`测试与架构专项.md` §1) | +| 网络 | 5 | N-2(49s 日志静默,**低风险观察项**)· N-3(L5 基线重验)· N-4(pending 二期)· N-16(A2/A4 负向台架)· N-17(探针窗口心跳缺口) | +| 存储 / 日志 | 3 | **S-3(USART1 TX-only 根治)** · S-4(dhara 重试自旋)· S-5(可选全序列卷锁) | +| 数据正确性 | 1 | **N-21(dhara `is_free`/`copy`,全簿最高风险)** | +| 结构 / 健壮性 | 9 | N-20(Error_Handler 死循环)· N-22(写回 flush)· N-23(注册校验残余白名单)· N-24(日志逐条开关)· N-25(性能/语义簇)· N-26(NAND DMA 断言)· N-27(内存优化簇,三件套 ✅ 前半落地)· N-28(Flash 死代码簇)· **N-33(栈余量收口,W1)** | +| 仓库 | 1 | N-29(`.git` 149 MB,**已决策暂不处理、登记观察**) | +| 硬件 | 1 | N-18(V0.2 改版项) | +| 测试 / 基础设施 | 6 | T-7(CI runner + 推送闭环 + 第二机交叉验证)· S12(断言宏)· S1-c(可选单测)· A-4(采集链路测试约束,RS485 侧随 B2 暂缓)· A-5 残余(CI 体积门禁盲区)· D7(串口助手 A6/A7 待 CH340) | +| UI | 1 簇 | U 簇(27 页标准化,**本轮不排期**) | +| **暂缓** | 1 簇 | **B2 RS-485 整簇(2026-10-12 用户决定,`暂缓-RS485.md`)** | + +> **网络侧另有 E-xx 未闭环项**(E-12/15/16/23/24/25/26/27/28/30/47/48/49/50/58/59/61,共 17 项) +> 与 1 项回归监督(E-62),详见 `../问题记录/错误码传播审计.md` §3——其中 **E-49/E-50 随 RS-485 暂缓**。 +> 本文件夹不重复登记。UI 标准化簇与覆盖率缺口(lftpd/dhara 移植层无主机测试)为整簇排期项,见 §2 W6 / N-21。 + +### 1.1 结构性判断(排序依据) + +1. **用户决定(2026-10-12):先把 ADC 采集、保护等相关功能完成**——"转发 + 保护"两环代码已落地(B4/B5,2026-10-12),但**尚未台架闭环、功能面不完整**(定值不落盘、无频率保护、无动作记录/SOE、栈安全线两处 FAIL)。新 W1 = 采集→相量→保护→上送链路的**收尾与闭环**(B6~B10 + N-33,见 `W1-采集保护收尾.md`),其余波次顺延。**RS-485 对下采集(B2)按用户决定暂缓(2026-10-12,`暂缓-RS485.md`),主线不再等它**。 +2. **数据正确性最高风险 = N-21**(dhara `is_free` 只查页首 64 B / `copy` 不处理哨兵):低概率、后果重(数据丢失),且至今只能靠人工读代码发现——修复须配主机侧移植层测试(一并补报告"覆盖率"缺口)。 +3. **内存预算红线已机械化(2026-10-12,W1-0)**:`mem-budget` 钩子拦 RW_IRAM1/RW_CCM >92%,子系统配额 / CCM 台账 / 趋势基线 → `docs/内存管理/RAM预算台账.md`;实际优化 R-9→R-2 排 W3(**B5 堆扩容 48→56 KB 后 CCM 只剩 ≈8,160 B**,解锁链见 `docs/内存管理/RAM预算台账.md` §3)。 +4. **验证债仅剩窗口性事项**(D2/D5/D6/D7 + P1-10 收尾),不单独排期、逢窗口顺手做。 + +--- + +## 2. 统一推进顺序(全项目唯一权威) + +> **本节是全项目所有工作流唯一的权威顺序**:`产品需求设计文档.md` §9.2、`../算法/继电保护功能复现规划.md`、 +> `项目审查与优化建议报告.md` §0.3 中的路线图均为局部视图,**顺序变更只改本节,其它文档只留指针**。 +> 排序原则:**安全风险 > 产品功能主线 > 数据正确性 > 预算/结构债 > 验证常态 > UI 扩展 > 仓库决策**(当前无未闭环安全项)。 +> 波内详表在挂接文件里(`W1-采集保护收尾.md` / `排期详述.md` / `暂缓-RS485.md`),但**波次增删与先后只改 §2.0**。 + +### 2.0 波次总览 + +| 波次 | 内容 | 为什么在这 | +|---|---|---| +| **W1 采集+保护收尾** | **里程碑制 + 并行道**:M1 台架闭环(台架道 B6 ✅→N-33)∥ M2 功能面(代码道 B7 ✅→{B8∥B9})→ M3 送检(B10,Fixed Date)——任务卡/判据/写范围见 `W1-采集保护收尾.md` | **用户决定(2026-10-12):先把 ADC 采集、保护等相关功能完成**。B4/B5 代码已落地但未台架闭环、功能面不完整(定值不落盘 / 无频率保护 / 无动作记录 / 栈两处 FAIL)——本波收口到"可用 + 可验收";真约束只有**台架**与**送检窗口**两个稀缺资源,其余任务按写范围切分并行 | +| **W2 数据正确性** | **N-21** dhara `is_free`/`copy`(报告 N-12/13) | 低概率、后果重(数据丢失);修复须配主机侧移植层测试,一并补报告"覆盖率"缺口 | +| **W3 预算与内存** | **N-27 后半**:R-9 池 40→16 KB → R-2 后半(54 KB→CCM)→ R-3~R-6 逐项评估 | 依赖 W1 三件套产出的台账数据;**B5 堆扩容后 CCM 只剩 ≈8 KB**(56 KB 堆),R-2 后半更被堵死(解锁链见 `docs/内存管理/RAM预算台账.md` §3) | +| **W4 结构债** | **N-20 → C6(S-3)→ C3(N-2+N-24)→ C1 pending 二期 → C5(含 N-23 残余)→ N-22/N-25/N-26/N-28**(详表 `排期详述.md` §1) | 无紧急,按性价比排:C6 灭 IWDG 误复位机制根源;C1 体量大、验证重,放后(N-23 钩子已落地 2026-10-12,残余并入 C5) | +| **W5 验证常态** | P1-10 收尾 · T-7 CI · D2 N-3 · D5 N-16 · D6 N-17 · D7 串口助手 · S12 · S1-c · A-5(详表 `排期详述.md` §2) | 窗口性,逢机顺带,不单独排期 | +| **W6 UI 扩展** | U4 残余(golden + 字符集差集挂门禁)→ U1 令牌层 → U2/U3 → **27 页**落地 | 产品驱动、体量大,需单独立项(顺序按 `docs/问题记录/项目审查与优化建议报告.md` §6.5);**本轮不排期(2026-10-11 已确认)** | +| **W7 仓库决策** | **N-29** `.git` 149 MB + 图片 36.3 MB | **已决策(2026-10-11):暂不处理、登记观察**——推送/协作前重评(`git filter-repo` 历史重写影响未推送提交与协作者) | +| **暂缓** | **B2 RS-485 整簇**(`暂缓-RS485.md`) | **用户决定(2026-10-12):当前不处理 RS-485** | +| **硬件改版** | N-18 8 路电气量全接 TPAFE(V0.2) | 下一版硬件;固件侧已用语义映射隔离,改版只动映射表 | + +### 2.1 顺序一图 + +``` +W1 收尾 台架道: B6✅→N-33(M1) ∥ 代码道: B7✅→{B8∥B9}(M2) → B10 送检(M3, Fixed Date) 详表 W1-采集保护收尾.md + (B4 IEC104 ✅ / B5 protectTask ✅ / B6 台架复测 ✅ / B7 定值持久化+软压板 ✅ 2026-10-12 已销项;台账三件套 ✅) + ↓ +W2 数据 N-21 dhara is_free/copy(配主机侧移植层测试) + ↓ +W3 预算 R-9 池瘦身 → R-2 后半(54KB→CCM)→ R-3~R-6 评估 + ↓ +W4 结构 N-20 → C6(S-3) → C3(N-2/N-24) → C1 pending 二期 → C5(含 N-23 残余) → N-22/25/26/28 + ↓ +W5 验证 P1-10 · T-7 CI · D2/D5/D6/D7 · S12 · S1-c · A-5 + ↓ +W6 UI U4 残余 → U1 令牌层 → U2/U3 → 27 页(单独立项,本轮不排期) +W7 仓库 N-29 .git 149MB(已决策暂不处理) 硬件:N-18 V0.2 +暂缓 B2 RS-485 整簇(2026-10-12 用户决定,见 暂缓-RS485.md) +``` + +--- + +## 3. 维护约定 + +- 本文件夹是**全项目唯一的未闭环事项登记簿**——"盘点 + 排序 + 明细"三合一,明细按职责拆进本文件夹各文件(2026-10-12 解耦),不再分散到其它文档。 +- **并行写纪律**:任务分支(worktree)**不改本文件夹任何文件**;任务卡状态列(⬜/🟦)与销项一律由 Lead 在主工作树更新。解耦的收益 = 不同波次 / 不同登记之间互不冲突,**不是**任务分支可以写。 +- **任一项完成后直接删除对应条目**(先销本文件夹条目,再清理其它文档对本编号的引用);**不留变更记录**,历史见 `git log` 与各模块文档。 +- 已闭环条目的**结论与证据沉淀进模块文档**,本文件夹不再重复。 +- 编号一律**沿用且不回收**(P-x / N-x / S-x / T-x / S1x / A-x / B·C·D 前缀);`错误码传播审计.md` 的 E-xx 独立维护。 +- **引用审查报告编号一律带文档名**(报告 N-16 ≠ 登记簿 N-16、报告 S-3 ≠ 登记簿 S-3); + 报告并轨项用本登记簿新编号(N-19~N-29 + U 簇),出处列保留报告编号。 +- **§2 是全项目唯一的推进顺序权威**:`产品需求设计文档.md` 等文档中的路线图只留指针,顺序变更只改 §2.0(详表文件跟着挪,不改顺序)。 +- **暂缓块(`暂缓-RS485.md`)不是销项**:条目仍在簿,只是不占波次;重启或关闭时更新该文件与 §1/§2.0。 +- 新增发现按既有惯例分流:能立即修的进 §2 波次(W4 C5);需排期的进 `登记簿.md` / `测试与架构专项.md` 对应编号空间;跨版本观察的进 `登记簿.md`。 +- 上游引用关系:`问题记录/项目审查与优化建议报告.md`(体系层评审)、`问题记录/错误码传播审计.md`(E-xx)、 + `测试说明/实机测试实施指南.md`(执行流程)——三者均不在本文件夹合并范围。 diff --git a/docs/任务规划/W1-采集保护收尾.md b/docs/任务规划/W1-采集保护收尾.md new file mode 100644 index 00000000..6aaf51af --- /dev/null +++ b/docs/任务规划/W1-采集保护收尾.md @@ -0,0 +1,70 @@ +# W1 采集 + 保护收尾(当前波次任务卡) + +> **本文件是全文件夹改动最频繁的规划文件**:当前波次的里程碑 / 并行道 / 任务卡。 +> 状态变更(⬜ 未开始 → 🟦 进行中 → 销项删行)只改本文件;**排序权威(波次总览)在 `docs/任务规划/README.md` §2**。 +> 方法论出处与执行层映射(工作树 / 锁 / 并行道)→ `../环境与部署/并行任务工作树方案.md` §7.3。 + +## 1. 背景(已闭环,只留指针) + +B0 产品定义 2026-10-10 确认——上行 **IEC 60870-5-104**(以太网 TCP 2404,遥测短浮点系数 1)/ +持续采集 / **1000 Hz(20 点/周波)** / RS-485 对下 ModBus RTU,结论详见 +`../算法/继电保护功能复现规划.md` §1.5 与 `../产品需求/产品需求设计文档.md` §5.9/§5.10/§9; +B1 ADC 采集链路已 `@test` 实机闭环;B3 纪律门禁已固化进 `AGENTS.md`; +**B4/B5 已销项(2026-10-12)**——证据见 `../子系统设计/IEC104上行设计说明.md`(82 断言 + 钩子)与 +`../算法/继电保护功能复现规划.md` §7 变更记录(protectTask 20 ms 判据节拍 + L5-4 采样闭锁 + +IWDG bit8 + 堆 48→56 KB + prot_elm 28 用例); +**B6/B7 已销项(2026-10-12)**——B6 证据 = `../测试说明/boot_log_data/README.md` §3 +(@test PASS + 门控缺陷 MISS→FIX 快照 + PC 主站 18/18,工具 `test/host/iec104_master.py`); +B7 证据 = `../子系统设计/IEC104上行设计说明.md` §3.4 + boot_log_data B7 行 +(定值/压板跨复位保持 + YK12→YX 联动,主站 42+8 判据 + 主机 63+94 断言)。 + +**本波目标(用户决定 2026-10-12)**:把"采集→相量→保护→上送"链路从"代码落地"收口到 +**"台架闭环 + 功能面完整 + 可送检"**。规划方式 = **里程碑(时间驱动、判据退出)× +并行道(按资源约束切分)× 任务卡(五字段)**。 + +## 2. 里程碑(时间驱动:功能可顺延,里程碑不拖延) + +| 里程碑 | 退出判据(全部满足才算完成,随后销项) | 服务分类 | +|---|---|---| +| **M1 台架闭环** | N-33 `stack_report` 0 FAIL(含 `@test` 复测腿);B6 `@test` PASS ✅ 2026-10-12(min_ever 背书就绪) | Standard | +| **M2 功能面完整** | B8/B9 全部合入 + `ci_local` 全绿 + PC 客户端往返含"SOE 时标"("定值掉电保持" ✅ B7 台架 2026-10-12) | Standard | +| **M3 可送检** | B10 注入判据 + 第三方主站全流程通过,证据归档 | **Fixed Date**(送检窗口一到全道让路;窗口前 2 日特性冻结,只修稳定——Zephyr RC 语义) | + +## 3. 并行道(台架 = 约束资源永不空转;代码道 = worktree 并行、绿即合) + +```mermaid +graph LR + subgraph BL[台架道 🔴 串行独占 bench.lock] + N33[N-33 栈收口+复测] + end + subgraph CL[代码道 🟢 worktree 并行 绿即合] + B8[B8 频率保护] + B9[B9 动作记录/SOE] + end + N33 --> M1((M1)) + B8 --> M2((M2)) + B9 --> M2 + M1 --> B10[B10 送检窗口 🟡 Fixed Date] + M2 --> B10 +``` + +## 4. 任务卡(五字段:DoD / 硬依赖 / 资源 / 写范围 / 状态;完成即销项删行) + +> 资源图例:🔴 台架(COM6/ST-LINK,`bench.lock` 独占)· 🟡 外部(标准源/第三方主站)· 🟢 纯软件(可并行)。 +> 状态:⬜ 未开始 · 🟦 进行中(销项行直接删除,见 `docs/任务规划/README.md` §3 维护约定)。 +> ⚠ **写范围相交**:B7 已先合入(2026-10-12,`App/algo/prot_set.*` 与 `App/protocol/iec104/iec104_pts.c` 定值段已释放);B8∥B9 写范围互不相交,可直接并行。 + +| 序 | 任务 | DoD(可验证验收判据) | 硬依赖 | 资源 | 写范围 | 状态 | 出处 | +|---|---|---|---|---|---|---|---| +| **N-33** | **栈余量收口**(defaultTask 余 1080 B < 30% 线 / timeSyncTask 余 784 B < ISR 底线,2026-10-12 实测;**既有债非 B4/B5 引入**;链 = dhara GC→控制台回显尾→`(Cycle)`) | `stack_report.py` 9 任务 0 FAIL + `@build` 0/0;若抬栈:台账 §6 登记 + `@test` 复测 PASS | B6 ✅(min_ever=10000 已背书 2026-10-12,AGENTS 抬栈门禁) | 🟢(瘦链调研)+ 🔴(复测腿) | `Src/freertos.c`(USER CODE)/ 瘦链目标文件 | ⬜ | 2026-10-12 `stack_report.py` 实测 | +| **B8** | **频率测量算法 + 过频/低频保护(P2 剩余)** | 频率测量主机单测(45~55 Hz 扫描,精度判据写入文档)+ OF/UF 实例单测;`@build` 0/0;心跳行显示频率值。OV/UV/3U0 实例 B5 起已在跑,注入判据归 B10 | — | 🟢 | `App/algo/phasor.*`(或新 `freq.*`)、`App/algo/prot_elm.*`、`App/algo/prot_set.*`(定值行) | ⬜ | 保护规划 §3.3 L3-5、保护规划 §7 P2 | +| **B9** | **动作记录 + 时标 SOE(L4)** | SOE 记录编解码主机单测 + iec104 单测(M_SP_TB_1);`@build` 0/0;protectTask 静态链不变(动作路径无 NAND I/O,记录走队列异步);**台架腿**:动作边沿 → `/log` 记录可见。出口跳闸仍否定确认(R7 口径不变) | —(代码);验证腿 🔴 | 🟢 + 🔴 | `App/log/`、`App/protocol/iec104/iec104_asdu.*` + `iec104_pts.c`(YX 时标段)、`App/task/protect_task.c` | ⬜ | 保护规划 §3.4 L4、保护规划 §7 P4;`../子系统设计/IEC104上行设计说明.md` §3.2 注 | +| **B10** | **标准源注入 + 第三方 104 主站往返(送检窗口)** | 标准源注入:L1-5 标定复核 / L1-7 等周波 / P1 过流·过负荷 / P2 电压 + 频率类判据全通过;第三方主站总召/周期/变位/遥控全流程(`../子系统设计/IEC104上行设计说明.md` §7/§8 核对单);证据归档 | M1 + M2 | 🟡 | —(窗口内缺陷修复走 Expedite 类) | ⬜ | 保护规划 §7 P1/P2 验收;`../子系统设计/IEC104上行设计说明.md` §7/§8 | + +## 5. 并行规则(执行层映射 → `../环境与部署/并行任务工作树方案.md` §7.3) + +> ① **WIP ≤ 2**:每道最多 1 个进行中(台架道 + 代码道各一),先完再开(Kanban); +> ② worktree 分支**存活 ≤2 天、绿即合**(`done` 预演合并;31 钩子强门禁是并行安全底座); +> ③ 写范围相交必须声明合并次序(见 §4 表注); +> ④ **里程碑不拖延、功能顺延下一轮**(时间驱动,Zephyr 发布语义); +> ⑤ 台架道永不空转:代码道在跑时,为下一个台架任务备料(约束优先,TOC)。 diff --git a/docs/任务规划/基线与待确认.md b/docs/任务规划/基线与待确认.md new file mode 100644 index 00000000..7c77f53a --- /dev/null +++ b/docs/任务规划/基线与待确认.md @@ -0,0 +1,93 @@ +# 基线与待确认(设计参考,少改动) + +> 本文件是**参考基线**,不是待办池:待确认项(§1)、设计基线(§2)、诊断基线(§3)、内存预算(§4)。 +> 待办的排序与登记在 `docs/任务规划/README.md` §2 与 `docs/任务规划/登记簿.md`;**同一事实以内存管理文档为权威**(§3/§4 只是快照指针)。 + +## 1. 待确认(需手册/硬件/产品定义,暂不直接改码) + +1. TPAFE5160 是否有自动扫描模式(**已不影响**:1 kHz 硬件定时器触发已于 2026-10-10 由 **TIM3 更新中断**方案落地,不依赖自动扫描,见 `docs/子系统设计/ADC采样节拍设计说明.md`)。 +2. `APP_ADC_ENABLE` / `APP_RS485_ENABLE` 是否应在产品构建真实门控模块(当前宏已如实标注为"未接线占位",此处确认的是**是否接入真实门控**;B1 取数不依赖该宏。**RS-485 侧门控随 B2 暂缓,见 `暂缓-RS485.md`**)。 +3. RS-485 PD0 DE 的半双工方向语义需硬件实测确认(本次仅静态审查)——**随 B2 暂缓(`暂缓-RS485.md`)**。 + +## 2. 设计基线(供后续接入参考) + +### 2.1 pending 二期设计基线(ACCEPT + SEND) + +> **来源**:一期(RECV)架构现状见 `docs/网络/Net层阻塞模型架构设计.md`(含演进溯源附录); +> 本节仅保留**二期仍要用的设计基线**。 + +**一期已验证的机制骨架**(二期沿用,勿重新发明):独立静态 pending 表 + `net_pending_tick` +主循环节拍了结(五路:数据/到点/断连/同 fd 串行/重建取消必通知)+ 登记守卫(每 fd 至多一个、 +不覆盖、拒绝 netTask 自等待)+ 1s 看门狗强结算。 + +**二期扩展点**: + +| 项 | 目标 | 对应缺陷 | +|---|---|---| +| **ACCEPT pending** | accept 等待改为"登记 → CONNECT 事件完成",退役 100ms/200ms 轮询循环 | E-06(accept 轮询计时失真) | +| **SEND pending** | 发送续传改为"登记 → send_ready 完成",消除 1s/分片 stall 对队列的冻结 | E-02 残余(SEND 位置) | + +**二期门禁**(一期门禁的加强版): + +1. CH395F NET 层全套件(含一期未跑的 Phase 7 / Phase 10,视 RAM 预算); +2. L3/L5 基线重锁(一期已重锁 M3,二期须再锁); +3. `e02_probe.py` **探针族扩充**:close / reconfig / EOF / disconnect-during-pending 四专项; +4. `ftp_regression.py`(D20)+ E-06 假 425 场景**独立重跑**(一期是借 M3 压力档覆盖的); +5. UI 远端推流压力 + timeSync 三场景; +6. 产品 `@test` 闭环 + ST-LINK 全片回读; +7. `ram_report.py` / `stack_report.py` 前后对比(铁律); +8. 看门狗接 selftest 留痕 + 模拟器差距清单 +1(`net_socket_sim.c` 无法建模 pending)。 + +**已知代价(延续一期,需文档化)**:pending 完成判定随主循环节拍运行,被长操作占用时超时 +顺延(~1ms 级);二期把 SEND 也 pending 化后,该顺延面反而**收窄**。 + +**改动量估算**:再 ~300-500 行,集中在 `net_socket.c`;公开 API 不变。风险评级**中** +(执行器核心 + D 系列前科)——**大的是验证,不是代码**。 + +### 2.2 错误码通道四要素(CH395F 范式) + +> **来源**:`../问题记录/错误码传播审计.md`。GD5F 错误通道统一(E-10 / E-42 / E-53 / E-54 / E-56 / E-57) +> 即按此骨架落地(2026-10-03,套件回归 16/16 PASS)。**后续任何驱动 / 子系统建立错误通道时应沿用。** + +``` +① 分类:区分「超时 / 真故障 / 参数错」——不得混为一个返回码 +② 透传:底层错误码逐层上报,不吞不猜(禁 `(void)ret`) +③ 分级:可恢复(重试)/ 不可恢复(上报自检 + 降级)显式判定 +④ 留痕:不可恢复项写 syslog selftest,可恢复项计数并在 UI / 日志可见 +``` + +**关键纪律**(呼应 `AGENTS.md` 已有铁律):**自愈逻辑严禁把"名字 / 参数类错误码" +归入"卷损坏触发集"**——`.fschk` 无限重格式化事故即此类(2026-09-20)。 + +## 3. 诊断基线(供后续对照) + +> **2026-10-10 重采(栈)/ 2026-10-04 重采(堆)**:栈 = **运行时高水位 + armlink 静态最坏链双口径** +> (产品构建 `suite=NONE`,开机 + 空闲采样,串口 `[STACK]` + `stack_report.py`);静态区 = `.map` 受管快照。 +> 栈水位口径:2026-10-10 削链后 defaultTask 实测 **2152 B**、静态最坏链 **2776 B**,**未抬栈**即达标 +> (完整核算见 `docs/内存管理/RAM使用分析与优化方案.md`)。 +> ⚠ 下表为**历史快照**(2026-10-04/10-10 口径);**现行权威数字**看 `docs/内存管理/RAM预算台账.md` +> 与 `docs/内存管理/内存内容分配策略.md` §6(2026-10-12 B5 后:堆 56 KB、RW_CCM 87.9%)。 + +| 项 | 当前值(栈:2026-10-10;其余:2026-10-04) | 出处 | +|---|---|---| +| 产品构建 Flash (ER_IROM1) | 266,632 B / 1024 KB(25.4%);ROM 合计 **267,372 B** | `.map` 受管快照 | +| 产品构建 RW_IRAM1 | **84,960 / 131,072 B(64.8%)** | `.map` 受管快照(`RAM使用分析与优化方案.md` §2.1) | +| 产品构建 RW_CCM | 49,184 / 65,280 B(75.3%,历史当时值。2026-10-12 B5 后堆 56 KB / 87.9%) | 同上 | +| 实测堆 | free=8,664 / **min_ever=4,728 B**(历史当时值,48 KB 堆时代) | V5 串口 `[STACK] heap` | +| **defaultTask 栈峰值** | **2152 / 4096(余 1944 B = 47%);静态最坏链 2776 B(余 32%)** | `[STACK]` + `stack_report.py`(产品构建) | +| netTask 栈峰值 | 920 / 3840(余 76%);静态 1872 B | 同上 | +| ftpTask 栈峰值 | 740 / 4096(余 81%);静态 1216 B | 同上 | +| uiTask 栈峰值 | 4016 / 16384(余 75%);静态 1336 B | 同上 | +| timeSyncTask 栈峰值 | 792 / 2048(余量 61%) | 同上 | +| adcTask 栈峰值(B1,2026-10-10 新建) | **496 / 2048**(余 1552 B / 75%,台架 `@test` 实测);静态最坏链 504 B | 台架 `@test` + `stack_report.py` | +| testTask 栈峰值(产品态空转) | 496 / 8192(余量 93%) | 同上 | +| 队列响应性(挂起客户端) | 92.5ms(pending 一期) | `e02_probe.py` | +| M3 压力档帧周期 | med 60.1 / p95 66.4 / max 69.3ms,零尖峰 | `链路延迟测试说明.md` | + +## 4. 内存预算(支撑 §3 与定栈规则) + +现行权威数据 = `docs/内存管理/RAM使用分析与优化方案.md` v1.2(2026-09-21,§2.1 受管快照): +任务栈在 CCM 堆(defaultTask 1024w / netTask 960w / ftpTask 1024w / testTask 2048w / uiTask 4096w / timeSyncTask 512w / **adcTask 512w**); +产品构建 RW_IRAM1 84,960/131,072 B(64.8%)+ RW_CCM 49,184/65,280 B(75.3%,历史当时值。2026-10-12 B5 后堆 56 KB / 87.9%); +实测堆 `free=8,664 / min_ever=4,728` B(历史当时值,48 KB 堆时代);台账工具 `scripts/ram_report.py` + `scripts/stack_report.py`。 +**旧栈表/旧余量数字作废,勿引用。** diff --git a/docs/任务规划/排期详述.md b/docs/任务规划/排期详述.md new file mode 100644 index 00000000..68c04d30 --- /dev/null +++ b/docs/任务规划/排期详述.md @@ -0,0 +1,26 @@ +# 排期详述(W4 结构债 · W5 验证常态) + +> 波次总览与顺序权威在 `docs/任务规划/README.md` §2;当前波次任务卡在 `W1-采集保护收尾.md`。 +> 本文件只放**已排期但非当前波次**的详述表——低频改动,随窗口更新。 + +## 1. W4 结构债详述 + +> 波内顺序 = 性价比:先可靠性根治,后大体量重构;评估簇随窗口。 + +| 序 | 任务 | 说明 | 出处 | +|---|---|---|---| +| **N-20** | **`Error_Handler` 统一降级** | 一处初始化失败 = 整机挂死(只剩 IWDG 兜底)→ 记自检 + 禁用该子系统 + 其余照常。详见 `登记簿.md` | 报告 N-03c | +| **C6** | **USART1 TX-only 结构根治(S-3)** | 灭 ORE 僵尸机制根源(2026-10-02 台架 IWDG 误复位的机制根源):改 `.ioc` 令 USART1 不使能 RX / PA10 不复用为 `USART1_RX` → CubeMX 重生成 → 跑 `check_cubemx_survival.py`。结构性改动,需单开一轮 + 复跑台架闭环 | S-3 | +| **C3** | **存储侧日志管线(N-2 + N-24)** | 丢弃可观测性**代码已具备**(`console_log.c` `s_con_drop`+`[ERR] CONSOLE dropped`、`sys_log_mcu.c` 队列满 `DBG_ERROR`、TX 僵尸通道自愈);V5 30 min 日志**未复现 49s 静默**(运行期 >10s 静默全为 ~28s、均在 `e02_probe` 窗口内=N-17)。剩余:与 S8(非阻塞日志)合并评估 + 日志逐条 `f_open/f_write/f_close` 改造评估(N-24) | N-2 / N-24 | +| **C1** | **pending 二期(ACCEPT + SEND)** | 沿一期机制扩展;同期消化 E-25(net_select 语义)、E-24/E-27/E-28(跨上下文收口);基线重锁 + 探针族扩充 + 看门狗接 selftest。**设计基线见 `基线与待确认.md` §2.1** | N-4 | +| **C5** | 一档剩余 | E-15(ringbuf 定去留——**已确认零调用者,倾向删除**)、E-61(SD2506 注释订正)、E-16(cfg_file 段名长度)、E-26(UDP 定案,倾向移除)、E-23、E-30;**N-23 残余**(`KNOWN_GAPS` 白名单清理 + `app_cfg.h` 未接线开关,见 `登记簿.md`)。**E-49(RS485 DE 收尾)已移入 `暂缓-RS485.md`** | 一档/二档 | +| **N-22/25/26/28** | 评估簇 | 写回 flush / 性能与语义一致性 / NAND DMA 断言 / Flash 死代码——逐项见 `登记簿.md` 对应行,随窗口评估 | 报告各条 | + +## 2. W5 验证常态详述 + +| 序 | 任务 | 说明 | +|---|---|---| +| **D2** | N-3 L5 基线重锁 | 下次 Phase 13 窗口顺带做(低风险:阈值余量 >8×;**需 PC peer**) | +| **D5** | **N-16 专项负向台架窗口** | A2/A4 负向场景:PHY 拔线(需断 DTU 侧/关受管交换机端口)、A2 芯片级 `net_init` 失败(SPI 故障注入)、A4 短写压测 | +| **D6** | **N-17 探针窗口心跳缺口** | 定位 `time_sync` 监听重建为何阻塞 NET/FTP/UI(~1s 缺口,余量仅 ~0.7s);评估缩短重建重试或让掉线路径不阻塞其它任务 | +| **D7** | **串口助手 A6/A7 正路径补验** | 需 CH340 硬件接入后补验(`../测试说明/串口助手设计说明.md` 状态行 A1–A5/A8 已过) | diff --git a/docs/任务规划/暂缓-RS485.md b/docs/任务规划/暂缓-RS485.md new file mode 100644 index 00000000..0ae7547b --- /dev/null +++ b/docs/任务规划/暂缓-RS485.md @@ -0,0 +1,12 @@ +# 暂缓:RS-485 整簇(用户决定,2026-10-12) + +> **当前没有处理 RS-485 的打算**,以下条目整体冻结、不占波次;**重启 B2 时先回本文件评估前置,勿直接开工**。 +> 本文件**不是销项**:条目仍在簿,只是不占波次(`docs/任务规划/README.md` §3 维护约定)。 + +| 项 | 内容 | 重启时的前置 | +|---|---|---| +| **B2** | **RS-485 采集链路实现**(消化 E-50、E-49):双缓冲 + 序号(防包撕裂)+ DE 收尾(超时路径等 TC)+ ModBus RTU 状态机(`App/protocol/` 正是为此预留) | **E-59 门禁尚未满足**:先修 E-50(单缓冲包撕裂)、E-49(TX 超时拉 DE 未等 TC),修完驱动契约才允许接消费者 | +| E-49 | RS485 DE 收尾(原 C5 一档剩余,已移出) | 同上 | +| A-4 RS485 侧 | 采集链路测试可验证性约束(双缓冲 + 序号、DE 超时路径覆盖,`测试与架构专项.md` §2.3) | 随 B2 设计文档落地 | +| 待确认-3 | RS-485 PD0 DE 半双工方向语义硬件实测确认(`基线与待确认.md` §1 第 3 项) | 需硬件实测窗口 | +| N-25 中 DE 项 | `HAL_Delay(1)` 阻塞 DE | 随 B2 | diff --git a/docs/任务规划/测试与架构专项.md b/docs/任务规划/测试与架构专项.md new file mode 100644 index 00000000..389df5cc --- /dev/null +++ b/docs/任务规划/测试与架构专项.md @@ -0,0 +1,97 @@ +# 测试与架构改进专项(P1 收尾 / S1x / A-x) + +> 编号空间见 `docs/任务规划/README.md` §0;排期位置见 `docs/任务规划/README.md` §2(W5 验证常态)。 +> **S1~S14 / T-1~T-5 / A-1~A-7** 是 2026-10-02 版复审的**条目编号**——编号**不随条目删除而回收**; +> 其他文档若引用这些编号,其证据见对应模块文档与 `git log`。已完成事项的证据沉淀: +> `../测试说明/系统时钟单元测试说明.md`(S1-a)、`../测试说明/配置文件单元测试说明.md`(S1-b)、 +> `../测试说明/测试开发标准流程.md` §4.2.1(L0.5 适格台账)、`../测试说明/模拟器差距清单.md`(S13)、 +> `../测试说明/实机测试实施指南.md`(S4/S5/S7/S9/S11)、`../问题记录/GD5F2GQ5UE_Trap_Records.md`(S9)。 + +## 1. P1 收尾(1 项:P1-10 剩构建实测闭环) + +### P1-10 测试固件静态 RAM 预算(基线已重建、测试缓冲已瘦身,剩构建实测闭环) + +- **位置**:`MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct`(`RW_IRAM1 0x20000000 0x00020000` = 128KB;芯片 192KB 的上部 64KB 有意未映射,克隆芯片兼容性考量) +- **历史**(2026-09-15 审计 / 09-16 复现):产品构建静态镜像曾 = 123.6KB/128KB(余量仅 4.4KB);HW 全阶段固件的 PHASE5 三缓冲(3×4093B)→ 超 8.7KB(`L6406E` 实测);NET 固件的 32KB `s_rx_buf` → 超约 28KB。 +- **进展**(2026-09-21,已复核):① 预算基线重建(`docs/内存管理/RAM使用分析与优化方案.md` v1.2):FreeRTOS 堆迁 CCM,任务栈改由 CCM 堆分配;产品构建 RW_IRAM1 余 ~49KB;② NET 层测试 32KB `s_rx_buf`/`s_tx_buf` 已移除,改小局部缓冲;③ STORAGE 套件固件 2026-09-20/21 实测可构建可运行。 +- **剩余**:① CH395F 套件(`TEST_SUITE_CH395F`,HW 全阶段 / NET 层 7 配置)实际构建确认链接通过——PHASE5 三静态缓冲 3×4093B 仍在,理论余量足够但未实测;② `@test` 闭环;③ **不建议**扩 scatter 到 192KB(破坏既有克隆芯片兼容决策)。 +- **置信度**:high(基线数据来自 2026-09-21 map;测试固件构建未复验)。 + +## 2. 测试与架构改进专项(S1x / A-x) + +**未闭环一览** + +| 编号 | 事项 | 状态 | 详述 | +|---|---|---|---| +| **S12** | 模块级轻量断言宏 | ⬜ 待做(第 4 波按需) | §2.1 | +| **S1-c** | `sys_log` / `time_sync` 主机侧单测 | ⬜ 可选(价值递减) | §2.2 | +| **A-4** | ADC / RS485 采集链路的**测试可验证性约束** | ⏸ ADC 侧随 B1 闭环;**RS485 侧随 B2 暂缓(`暂缓-RS485.md`)** | §2.3 | +| **A-5 残余** | CI 体积门禁盲区(`mem-numbers` 在 CI 被跳过) | ⬜ 未消除 | §2.4 | + +### 2.1 S12:模块级轻量断言宏 ⬜ + +**动机**:`configASSERT` 只覆盖 FreeRTOS 内部;自研代码的"不该发生"靠散落的 `if`。 + +**设计要点**(**必须零生产开销**): + +```c +/* App/util/app_assert.h —— 示意,非最终实现 */ +#ifdef APP_ASSERT_ENABLE + #define APP_ASSERT(cond) do { if (!(cond)) { \ + DBG_ERROR("[ASSERT] %s:%d", __FILE__, __LINE__); vApplicationAssertFailed(__FILE__, __LINE__); } \ + } while (0) +#else + #define APP_ASSERT(cond) ((void)0) /* 生产构建:编译期消失,零符号零开销 */ +#endif +``` + +**⚠️ 关键纪律**: +- **不得**在中断 / 高频路径使用(`DBG_ERROR` 有代价); +- **不得**替代正常的错误返回(断言是"内部不变量",不是"外部输入校验"); +- 生产构建 `APP_ASSERT_ENABLE` **关闭**——与 `ENABLE_DESTRUCTIVE_TESTS` 同级策略。 + +**建议先在 1~2 处试点**(如 `net_socket.c` 的状态机不变量),验证收益后再推广。 + +**完成判据**:宏存在 + 生产构建 `@build` 0/0 且 `.map` 无新增符号;试点处有实际使用。 + +### 2.2 S1-c(可选):`sys_log` / `time_sync` 主机侧单测 ⬜ + +**范围**:`App/log/sys_log_mcu.c`(定长 64B 记录编解码;落盘部分需桩)、 +`App/sync/time_sync.c`(协议帧编解码;与网络耦合,需先判定可解耦面)。 + +**为何低优先**:两者边界密度低于 `sys_clock` / `cfg_file`,且已有真机路径覆盖 +(`@test` 标记 / `time_sync_server.py`)。**按剩余收益评估后再决定是否做**, +范式完全复用 `../测试说明/看门狗单元测试说明.md`。 + +**完成判据**:`test/host/<模块>/run_unit_test.py` 退出码 0 + 挂 pre-commit 钩子。 + +### 2.3 A-4:ADC / RS485 采集链路的测试可验证性约束 ⏸ + +**背景**:`adc_task.c` 已实现(B1,2026-10-10),`rs485_task.c` 仍是空桩,`App/protocol/` 只有 README 与 `.gitkeep`。 +本项不是"缺陷",而是**对上游 B2 的设计约束**(B1 已完成;**B2 暂缓中,见 `暂缓-RS485.md`——重启 B2 动手前必须先落实本约束**,否则后续无法补测)。 + +**约束**(建议纳入 B2 设计): + +1. **协议解析必须与硬件 I/O 分离**——`App/protocol/` 的解析函数应接受 + `(const uint8_t *buf, size_t len)` 并返回解析结果,使其成为 L0.5 的**天然适格对象** + (参考 `cfg_file` 的"核心 + 后端"模式); +2. **采集缓冲必须双缓冲 + 序号**(上游 E-49 / E-50)——否则消费者读到撕裂数据, + 且**撕裂在单测里测不出来**(时序相关),必须在设计上消除; +3. **DE 收尾必须有超时路径覆盖**(RS485 半双工特有陷阱)。 + +**完成判据**:B2 设计文档体现上述三条;协议解析函数有对应 L0.5 单测。(B1 侧:ADC 无协议解析面,其"同一时刻性"由 E-58 原子快照保证、已闭环。) + +### 2.4 A-5 残余:CI 体积门禁盲区 ⬜ + +**现状**:`mem-numbers` 钩子**在 CI 上被跳过**(CI 无 `.map`)→ **CI 不守体积门禁**。 +S14 已在 workflow 显式提示"体积门禁未执行"(`::notice::`)并在 `../环境与部署/CI配置指南.md` 声明, +但**盲区本身未消除**——看到 CI 全绿仍可能误以为体积也守住了。 + +**可选处置**: +- 让 runner 具备 Keil 后,在 CI 增 `@build` job(成本高,需授权与许可证); +- 或把"体积校验"固化为**提交前必跑的本地门禁**(`scripts/ci_local.py`,当前已是事实标准)。 + +**完成判据**:存在**强制的**体积校验路径(CI 侧或本地侧),且文档明确其边界。 + +> **注**:本项与栈预算是同一话题的两面——**具体栈风险(原 S-1)已于 2026-10-10 削链闭环** +> (核算见 `docs/内存管理/RAM使用分析与优化方案.md`),本项仍是**门禁缺口**。 diff --git a/docs/任务规划/登记簿.md b/docs/任务规划/登记簿.md new file mode 100644 index 00000000..54c35574 --- /dev/null +++ b/docs/任务规划/登记簿.md @@ -0,0 +1,53 @@ +# 登记簿(N-x / S-x / T-x 长期观察项) + +> 编号空间与"不回收"规则见 `docs/任务规划/README.md` §0;排期位置见 `docs/任务规划/README.md` §2 波次总览; +> 当前波次任务卡(带 DoD / 写范围 / 状态)在 `W1-采集保护收尾.md`。 +> **本文件是低频文件**:新发现按类别进对应表;销项 = 删行 + 清引用(`docs/任务规划/README.md` §3)。 + +## 1. 网络(CH395F / net 层) + +| # | 事项 | 发现 | 证据与现状 | 出处 | +|---|---|---|---|---| +| N-2 | **49s 全系统日志静默(defaultTask/NAND syslog 排空停滞)**——串口日志 49s 无行,期间 net 栈照常处理外部连接 → 日志管线停滞,队列满后各任务日志行被丢;系统功能不受损 | 2026-09-22(L3 台架 F2,gen 181) | **A/B 归因:既有存储侧现象**。**2026-10-03 降级为低风险观察项**:① 丢弃可观测性**代码已具备**(`console_log.c` `s_con_drop` + `[ERR] CONSOLE dropped`;`sys_log_mcu.c` 队列满 `DBG_ERROR`;UART TX 僵尸通道自愈);② V5 30 min **未复现 49s**(>10s 静默全为 ~28s、均在 `e02_probe` 窗口内=N-17)。**残余待办**:与 S8(非阻塞日志)合并评估(C3) | `错误码传播审计.md` E-02 阶段二 | +| N-3 | **L5 队列往返基线(1223/988µs)的 L3 后重验**——L5 测 DONTWAIT 队列全程;L3 对该路径逐位不变 → 机制上无影响,但基线数值属分片时代,严格重锁需跑一轮 Phase 13(需 PC peer) | 2026-09-22(L3 收尾留档) | 低风险:10ms/100ms 阈值余量 >8×;下次 Phase 13 窗口顺带重锁即可 | `链路延迟测试说明.md` §6.4 L5 注¹ | +| N-4 | **pending 机制二期:ACCEPT + SEND 阻塞路径 pending 化**——E-06、E-20、E-21 剩余分量共用该机制扩展;届时 L3/L5 基线再重锁、探针族扩充、看门狗接 selftest 留痕、模拟器差距清单 +1 | — | 一期(RECV)已落地并入主线;架构现状见 `Net层阻塞模型架构设计.md`,二期设计基线见 `基线与待确认.md` §2.1 | `错误码传播审计.md` 第二档 #2 | +| N-16 | **A2/A4 专项场景台架验证待办**(正常路径已实测零回归):① PHY 拔线负向测试——断链需断 **DTU 侧**线缆或关**受管交换机端口**;② A2 芯片级 `net_init` 失败分支(需 SPI 故障注入);③ A4 短写压测(需远显客户端 + UI 推流压力) | 2026-10-03(台架拓扑 = CH395↔交换机,拔线操作受限) | 待专项台架窗口 | `实机测试实施指南.md` §10 | +| N-17 | **`time_sync` 30s 挂起客户端掉线 + 监听重建期间,网络任务心跳缺口达 800ms~1s(声明 stall 但未复位)。**待办**:定位监听重建为何阻塞 NET/FTP/UI;评估缩短重建重试(5×2s)或让掉线路径不阻塞其它任务 | 2026-10-03(V5 台架) | V5 长跑 44 次 `task stall: mask=0x00000001`,**100% 与 `e02_probe` 的 30s 挂起客户端触发 `time_sync: no TIME packet in 30s, drop conn` → 监听重建(约 4s)重合**;隔离实验(仅 FTP,120s)**0 次 stall** → 探针场景专属。缺口(~1s)< IWDG 最坏超时(1701ms),余量仅 ~0.7s。 | `实机测试实施指南.md` §4;`../子系统设计/看门狗设计说明.md` | +| N-32 | **CH395F 监听生命周期根治(B10 残余)**——现仅靠对账④兜底(提交 `4f2cc8b`)。根治两步:**P1 把"会话已结束"从边沿改成电平**(`net_sock_t` 增 `session_dead`;`net_recv_locked` DONTWAIT 判据改 `state==CLOSED \|\| session_dead`;`ui_net_sink.c:563` 补 `g_client != g_listen` 护栏;accept 门控换 `net_listener_armed`);**P2 单一所有者**(新增 `net_listener_pump`,删 `net_accept_locked:1346`/`net_recv_locked:1884`/`net_pending_tick:2451` 散点 relisten;消灭 netTask 内 500 ms 忙等 `:2597-2616`/`:1115-1126`)。**已定案不走芯片多连接模式(A′ 出局:不支持多监听)** | 2026-10-12(B10 定案;⚠ ④ 有副作用——抢先 relisten 会把慢消费者的 EOF 永久吞掉,新客户端数据被旧会话静默吸收,待 P1 消除) | 原理 + 证据附录(实测数据 / 官方例程所有权 / 状态机 15 迁移点 / 三服务契约不一致表)+ **决策记录 D1~D7** + 可勾选执行清单 → `../子系统设计/网络Socket监听生命周期设计说明.md` §7~§9;现象与回归见 `../问题记录/CH395F_Trap_Records.md` B10 行 | + +## 2. 存储 / 日志管线 + +| # | 事项 | 发现 | 证据与现状 | 出处 | +|---|---|---|---|---| +| S-3 | **USART1 TX-only 结构根治(灭 ORE 僵尸机制)**——ORE 锁存在外设软复位后毫秒级重锁(PA10 悬空 + RX 使能),是 2026-10-02 台架"背靠背 drain 拉长主循环 → 喂狗节奏饥饿 → IWDG 误复位"的**机制根源**。现仅有缓解(单次 drain 250ms 预算 + 发送故障后 300ms 冷却),**未根治** | 2026-10-02(台架 gen-288) | **待办**:改 `.ioc` 令 USART1 为 TX-only → CubeMX 重生成 → 跑 `scripts/check_cubemx_survival.py`。结构性改动,需单开一轮并复跑台架闭环 | `../子系统设计/控制台单写者日志设计说明.md` §12(含 O4) | +| S-4 | **dhara 重试自旋残留**——`try_recover()` 的 `DHARA_MAX_RETRIES`(8) 自旋在坏块密集时仍可能长时间占用 defaultTask(喂狗点) | 2026-10-02 | 低优先:现有 `vol_lock` + 喂狗节奏缓解;若台架再出"存储侧喂狗饥饿"再排期 | `Lib/dhara/map.c` | +| S-5 | **可选全序列卷锁**——现卷锁仅覆盖 `syslog` / `cfg` / `lftpd` 三条路径(`vol_lock_wait(0)` try 语义),其它潜在访问点(如 UI 读日志)未纳入 | 2026-10-02 | 待评估:先确认是否真有第二写者;无则关闭本项 | `App/fs/vol_lock.{c,h}` | + +## 3. 硬件(V0.1 缺陷 → V0.2 改版项) + +| # | 事项 | 发现 | 证据与现状 | 出处 | +|---|---|---|---|---| +| N-18 | **8 路电气量分属两片转换器(V0.1 硬件缺陷,需下一版硬件修改)**——UA/UN/IA 走 TPAFE5160(16 位、8 通道同步、±5 V 双极性),UB/UC/IB/IC/IN 走 STM32 ADC(12 位、顺序扫描、0~3.3 V 单极性):① 两片转换器无共同采样触发 + STM32 侧顺序扫描(9 通道一轮 ≈ 67 µs)→ 跨转换器通道**无同一时刻性、相位关系不确定**,主板侧功率/功率因数/相角/对称分量类运算不成立;② 同族电气量精度与量程不一致(LSB ≈ 152.6 µV vs ≈ 806 µV,且 STM32 侧前端无中点偏置、负半周削顶);③ 语义与硬件归属交叉,固件映射/UI 通道序/自检注入须逐通道查表绕开 | 2026-10-10 定性更正(**撤销** 2026-10-09 建档时"有意分配、不要修正"的说法) | **V0.2 改版要求**:8 路电气量 UA/UB/UC/UN/IA/IB/IC/IN **全部接 TPAFE5160**,与 TPAFE 上 5 路备用路(AIN5/13/7/15/12)对调串联电阻在转换器一侧的目的地——元件数量与料号不变,属走线级改动。**V0.1 现板软件口径**:主板侧只做逐通道独立监视,禁止跨转换器做相位/功率/功率因数/矢量类运算(此类量取子板计量模块结果)。固件侧消费者已落地(B1,2026-10-10,按语义映射表取数)→ V0.2 改版只改映射表内容,上层语义与代码不动 | `../硬件平台/ADC通道映射说明.md` §1/§7;`../硬件平台/V0.1_网表详细分析报告.md` §1.4 V10、§6-3 | + +## 4. 测试与验证基础设施 + +| # | 事项 | 优先级 | 问题与证据 | 处置方向 | 出处 | +|---|---|---|---|---|---| +| T-7 | **CI runner 未就绪(部署前置)** | 中 | `.gitea/workflows/ci.yml` 已落地,但 CI 仅在 runner 就绪后才真正生效:① Gitea 实例 `[actions] ENABLED = true`;② 注册 Windows runner(labels 含 `windows`),装 Python 3.x / cppcheck / pre-commit / MSVC;③ runner 就绪后**推送远端跑一次两 job 闭环验证**(当前 21 个提交未推送,CI 从未真实执行);④ **第二机交叉验证**(不同工具安装路径,验 `DTU_*` 环境变量方案,→ `../环境与部署/本地工具路径环境变量方案.md` 变更记录 TODO) | runner 未就绪前,**提交前仍须在本地跑 `scripts/ci_local.py` 或 `pre-commit run --all-files` 自查**(L0 唯一保障,不能因"已配 CI"而放宽) | `.gitea/workflows/ci.yml`;`../测试说明/测试开发标准流程.md` §9 | + +## 5. 审查报告并轨(2026-10-11;⚠ 报告编号与本登记簿同名不同义,出处列保留报告编号,引用一律带文档名) + +| # | 事项 | 影响 / 现状 | 处置方向 | 波次 | 出处 | +|---|---|---|---|---|---| +| N-20 | `Error_Handler` 仍 `__disable_irq(); while(1){}`(`Src/main.c:274-282`) | 一处初始化失败 = 整机挂死(只剩 IWDG 兜底),无自检日志 | 统一降级策略:记自检 + 禁用该子系统 + 其余照常 | W4 | 报告 N-03c | +| N-21 | **dhara 两处**:`dhara_nand_is_free()` 只查页首 64 B;`dhara_nand_copy()` 不处理 `DHARA_NO_PAGE` 哨兵 | 映射恢复误判 → **数据丢失风险**(低概率、后果重);哨兵页号被当真实页 → 未定义行为;至今只能靠人工读代码发现 | 整页(或首/中/尾)扫描 + 返回 `DHARA_E_UNSUPPORTED`;**必须配主机侧移植层单测**(一并补报告"覆盖率"缺口) | **W2** | 报告 N-12/N-13 + §7 | +| N-22 | 写回缓存无空闲/掉电强制 flush(整页写与换页已 flush) | 写回窗口内掉电丢最近日志/配置写 | defaultTask 空闲 N ms flush | W4 | 报告 N-14 | +| N-23 残余 | **钩子已落地**(2026-10-12 `registration-check` 挂 pre-commit,存量缺口走 `KNOWN_GAPS` 白名单只 WARN);**残余** = 白名单清理:`rs485_task.c`(随 B2 暂缓一并处置:删除或注册)、`lftpd_log.c`;`app_cfg.h` 未接线开关(C5) | 白名单不清 = "写了不编译"盲区仍在 | 逐项清白名单(`--strict` 供审计复跑);开关随 C5 处置 | W4(C5) | 报告 N-06 残余 + 报告 S-3 | +| N-24 | 日志每条 `f_open+f_write+f_close`(`sys_log_mcu.c:123-140`) | 每条日志 2–4 次 NAND 页读写;UI 翻记录逐条 open/seek/close | 与 C3 日志管线合并评估(现仅 `FIL` static 化省 528 B 栈) | W4 | 报告 N-16(**≠ 本登记簿 N-16**) | +| N-25 | 性能/资源建议簇:PASV 每次新建监听;`NET_RECV_TIMEOUT_MS 30000`;PHY 强制 100M 无自协商回退;`NET_MAX_SOCKETS 8` 与单连接语义不符;`HAL_Delay(1)` 阻塞 DE(**DE 项随 B2 暂缓,见 `暂缓-RS485.md`**) | 次级性能/语义一致性 | 逐项见 `../问题记录/项目审查与优化建议报告.md` §4.2/§4.5/§4.7,随窗口评估 | W4 | 报告 N-09/P2-8/PHY/P3-x/rs485 | +| N-26 | NAND 直 DMA 调用方缓冲(已加运行时守卫,阈值 1024) | CCM 缓冲被 DMA = 静默传错(与 ADC DMA 不可达 CCM 同类坑) | 补编译期/启动期地址断言 + **新铁律:堆缓冲不可被 DMA 直接寻址** | W4 | 报告 N-11 | +| N-27 | **内存优化簇**:LVGL 池 + UI 两份缓冲 54,146 B 仍在 SRAM1,CCM 只剩 ≈8,160 B(2026-10-12 B5 堆扩容 48→56 KB 后) | R-2 后半段(迁 CCM)被堵死 → 后续功能预算看不清 | **三件套 ✅ 2026-10-12 落地**(`ram_baseline.py` 四模式 + `ram_baseline.tsv` 趋势 + `docs/内存管理/RAM预算台账.md` + `mem-budget` 钩子 92% 门禁)→ **W3** 再做 R-9(`LV_MEM_SIZE` 40→16 KB,池水位门禁数据已具备)→ R-2 后半;R-3/R-4/R-5/R-6 逐项评估(R-6 前置已满足) | **W3** | 报告 R-2 剩余/R-3~R-6/R-9/§5.5 | +| N-28 | Flash 死代码评估簇:剩余 ≈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 TU(F-1/F-2/F-3 已闭环,省的是编译时长) | 编译时长 + 文档噪音 | 逐项实测再关;⚠ `mask`/`mask_rect`/`grad` 是文字与矩形绘制必经路径,**不可盲关** | W4 | 报告 F-4/F-5/F-6 | +| N-29 | **仓库膨胀**:`.git` 149 MB + 图片 36.3 MB | clone/备份慢,且持续恶化 | 降采样 / 拆 submodule / LFS 三选一 **+ 一次 `git filter-repo` 历史重写**。**已决策(2026-10-11):暂不处理、登记观察**——推送/协作前重评(届时未推送提交更少、重写代价最低) | **W7** | 报告 D-1 | +| N-33 | **栈余量收口:defaultTask / timeSyncTask `stack_report` FAIL**(2026-10-12 复测:defaultTask 静态 3016 B/余 1080 B < 30% 线,链含 dhara GC→控制台回显→`(Cycle)`;timeSyncTask 静态 1264 B/余 784 B < ISR 底线 1024 B)——**既有债,非 B4/B5 引入** | 深链 + ISR 嵌套场景栈溢出风险(保护链稳定性相关) | 抬栈(**B6 min_ever 已背书 2026-10-12**:台架实测 heap free=17936 / min_ever=10000 > 3072 告警线,证据 `../测试说明/boot_log_data/20261012_B6_prod_boot_log.txt`)或瘦链(控制台回显尾 / Error_Handler 环),详表见 `W1-采集保护收尾.md` | **W1** | 2026-10-12 `stack_report.py` 实测 | +| U 簇 | UI 标准化:无令牌层/组件库/页注册表(手写 `case` 仍在),**27 页**未落地;golden 未挂 pre-commit、字符集差集检查仍是约定 | 新增一页成本仍是 8 处手工改动 | 按 `../问题记录/项目审查与优化建议报告.md` §6.5:U4 残余(两脚本入门禁,可逢窗口顺带)→ U1 令牌层 → U2/U3 → 页面落地;U4 golden 可直接当 U1 验收工具。**已确认(2026-10-11):本轮不排期**,等功能主线出结果后按产品需求启动 | W6 | 报告 U1~U5 | diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 94963a88..92673e24 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -9,7 +9,7 @@ ## 1. 背景与目标 -`待处理内容.md` 已提出:"所有任务的内存都是往大的给的,导致内存不够用,要仔细排查内存的预算情况"。 +登记簿(原 `待处理内容.md`,现 `docs/任务规划/`)已提出:"所有任务的内存都是往大的给的,导致内存不够用,要仔细排查内存的预算情况"。 分析起点(2026-09-11 快照,逐符号见 `data/ram_report_20260911.md`): @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 269,940 B (263.61 KB) | 1,048,576 B (1024 KB) | 25.7% | -| RW_IRAM1 | 89,320 B (87.23 KB) | 131,072 B (128 KB) | 68.1% | -| RW_CCM | 49,184 B (48.03 KB) | 65,280 B (64 KB) | 75.3% | -| **Flash (ROM 合计)** | **270,712 B (264.4 KB)** | 1,048,576 B (1024 KB) | **25.8%** | +| ER_IROM1 | 223,724 B (218.48 KB) | 1,048,576 B (1024 KB) | 21.3% | +| RW_IRAM1 | 96,144 B (93.89 KB) | 131,072 B (128 KB) | 73.4% | +| RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | +| **Flash (ROM 合计)** | **224,772 B (219.5 KB)** | 1,048,576 B (1024 KB) | **21.4%** | 各区的容量与常量来源: @@ -48,7 +48,7 @@ |---|---|---| | 主 SRAM (0x20000000) | 128 KB(RW_IRAM1 Max `0x20000`) | scatter `STM32F407-Demo.sct:22` | | CCM SRAM (0x10000000) | 64 KB;FreeRTOS 堆于 2026-09-21 迁入(§1.2) | scatter `STM32F407-Demo.sct:25`;RM0090 §3.1.1 | -| FreeRTOS 堆 (heap_4) | 48 KB(49,152 B),**位于 RW_CCM**;实测 `free=8,784 / min_ever=4,848` B | `Inc/FreeRTOSConfig.h:71` `configTOTAL_HEAP_SIZE` | +| FreeRTOS 堆 (heap_4) | **56 KB(57,344 B)**,**位于 RW_CCM**;2026-10-12(B5)由 48 KB 扩容(protectTask/iec104Task 栈入堆,CCM 水位 87.9% < 92% 门禁);旧 48KB 池实测 `free=8,784 / min_ever=4,848` B(2026-10-03),新池 min_ever 待上板复测 | `Inc/FreeRTOSConfig.h:71` `configTOTAL_HEAP_SIZE`(L1:`.ioc` `FREERTOS.configTOTAL_HEAP_SIZE`) | | C 库堆/栈 (startup) | 512 B / 1 KB,启动期 main() 用 | map `startup_stm32f407xx.o` HEAP/STACK 节 | ### 2.2 静态大项排行(2026-10-10 当前,完整台账 `data/ram_report_20261010.md`) @@ -85,7 +85,7 @@ > **DMA 可达性**:第 4/6/7 项(NAND FTL、CH395F SPI2、GD5F BBT)为 DMA 缓冲,**必须留在主 SRAM**、禁止迁 CCM/SRAM3——权威清点与规则见 `内存内容分配策略.md`(本文不重复)。 > 2026-09-11 历史排行(`heap_4.o` 49,152 B 尚在 SRAM1、静态合计 122.7 KB 的旧布局)见 `data/ram_report_20260911.md`,仅供回溯。 -### 2.3 任务栈(从 48KB 堆动态分配) +### 2.3 任务栈(从 56KB 堆动态分配,2026-10-12 起;此前 48KB) ![任务栈预算](../images/任务栈预算.svg) @@ -93,6 +93,11 @@ > ✅ **本表已于 2026-10-10 重新基线**(产品构建 `suite=NONE`,开机 + 空闲采样,静态链取 `stack_report.py`)。 > 同日**增列 `adcTask`**(B1:ADC 采集消费者,512 words,静态最坏链 504 B / 余 75%)。 +> **2026-10-11 L2 相量层接入后复测**:adcTask 静态 **832 B** / 台架实测峰值 **928 B / 2048(余 54%)**, +> 增量为压环 + `phasor_run()`(`win[20]` 80 B)+ `log_phasors` 日志链;仍高于 1024 B 底线,7 任务全 PASS。 +> **同日 L1-6 有效性校验后抬栈 512→640 words**:`adc_health_report`→`syslog_post` 控制台回显尾把静态 +> 最坏链推到 **1080 B**(512 words 时余 968 B < 1024 B ISR 底线)→ 抬至 2.5 KB,余 **1480 B / 57%**; +> 堆 `min_ever` 7152 → **6640 B**(-512 B,符合预算)。 > 此前 2026-10-04 的 V5 30 min 重采曾把 defaultTask 推到 **3664B/4096(余量 432B ≈ 10%,登记 S-1)**; > 2026-10-10 的栈整改(**未抬栈**,见 §4 阶段 0 的"2026-10-10 复测")把它压回 **2152B/4096**,S-1 已销项。 @@ -104,7 +109,9 @@ | testTask | `2048*4` | 8KB | 测试套件(产品构建不创建;存储套件深链需要) | 未创建 / 静态 496 B | | defaultTask | `1024*4` | 4KB | FatFS 挂载/探针 + 配置引擎 + syslog + 栈水位探针 | 2152 B(余 47%)/ 静态 2776 B | | timeSyncTask | `512*4` | 2KB | TCP 时间同步 | 616 B(余 69%)/ 静态 **1024 B**(= ISR 底线,无再加深空间) | -| adcTask | `512*4` | 2KB | TPAFE5160 采样(1 ms 采样节拍触发 CONVST + 原子快照消费,B1 2026-10-10) | **496 / 2048**(余 1552 B / 75%,台架 `@test` 实测);静态 504 B | +| adcTask | `640*4` | 2.5KB | TPAFE5160 采样消费 + L2 相量 + L1-6 有效性校验(TIM3 1 kHz 节拍,B1→L2→L1-6 2026-10-11) | **952 / 2560**(余 1608 B / 62%,台架 `@test` 实测 2026-10-11);静态 1080 B | +| iec104Task | `640*4` | 2.5KB | IEC104 上行服务(2404 监听 + link 状态机,B4 2026-10-12) | 未上板 / 静态 **1456 B**(拆链→重建监听→net_bind→SPI 故障→日志尾,日志主导;512 words 时余 592 B < ISR 底线 → 抬至 640,余 1104 B / 43%) | +| protectTask | `512*4` | 2KB | 保护判据节拍(prot_elm_run 消费者 + L5-4 采样闭锁,B5 2026-10-12) | 未上板 / 静态 **872 B**(float 判据 + phasor 快照 + 日志尾,余 1176 B / 57%) | | idle(内核) | `configMINIMAL_STACK_SIZE=128` | 0.5KB | 空闲 | 未观测(未取句柄) | | timer(内核) | `configTIMER_TASK_STACK_DEPTH=256` | 1KB | 软定时器 | 未观测 | @@ -171,13 +178,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) @@ -192,7 +205,7 @@ | 堆测量 | `xPortGetFreeHeapSize()` / `xPortGetTotalHeapSize()` 随时可查;heap_4 支持 free 但有碎片化,需对比"理论余量 vs 实测 free"观察碎片 | FreeRTOS 内核源码 `heap_4.c`(工程内);[FreeRTOS 文档](https://docs.freertos.org/) | | 运行时统计 | `vTaskGetRunTimeStats` 需 `configGENERATE_RUN_TIME_STATS=1` + `portCONFIGURE_TIMERS_FOR_RUN_TIME_STATS`(占用一个硬件定时器),给出每任务 CPU 占比 | 本工程内核 `task.h:1694-1756` 注释 | | LVGL 缓冲 | 官方文档:PARTIAL 渲染模式"buffers can be smaller than the display to save RAM,**至少 1/10 屏**";单色 I1 格式缓冲 = `(W×H/8)+8`;双缓冲可并行渲染/搬运 | LVGL v9.2 文档 [Display interface](https://github.com/lvgl/lvgl/blob/release/v9.2/docs/porting/display.rst)(lvgl.io/docs/porting/display 的源文件) | -| STM32F4 CCM | F407 含 64KB CCM @0x10000000;**只能由 CPU 访问,DMA 不可达**,无 cache/ECC;适合纯 CPU 读写的大缓冲 | ST RM0090(STM32F405/407/415/417/42x 参考手册)§3.1.1 | +| STM32F4 CCM | F407 含 64KB CCM @0x10000000;**只能由 CPU 访问,DMA 不可达**,无 cache/ECC;适合纯 CPU 读写的大缓冲 | `docs/芯片手册/STM32F4xx参考手册_V18英文版_1-1000.md` §3.1(RM0090 原文该小节为 §3.1.1 CCM,本仓摘录只到 §3.1) | | FatFs | `FF_FS_TINY=1`:FIL 结构体去掉 `buf[FF_MAX_SS]` 私有扇区窗,改用卷内共享 9 字节窗口(FAT32),每打开文件省 512B | 本工程 `Lib/FatFs/ff.h` FIL 结构注释、`Lib/FatFs/ffconf.h:32` | | 静态占用测量 | Keil map 文件 "Image component sizes" 给出每个 .o 的 RO/RW/ZI 精确字节数;`fromelf --image_info xxx.axf` 可输出各 Region 用量 | 本工程 `STM32F407-Demo.map` "Image component sizes" 表(行号随每次重建变化,勿引用行号) | @@ -262,7 +275,8 @@ void vApplicationMallocFailedHook(void) - RW_IRAM1 Region 的 `Size` / `Max` → 主 SRAM 静态水位 - "Image component sizes" 表 → 每 .o 的 ZI/RW 排序 Top20 - 用法:`& '.venv\Scripts\python.exe' scripts/ram_report.py -o docs/内存管理/data/ram_report_<日期>.md`,把结果贴进改动说明 -- (待补)与基线 TSV 做 diff,任何对象 ZI 增量 >1KB 即高亮 +- ✅ **2026-10-12 落地**:`scripts/ram_baseline.py --trend` 与基线 TSV + (`scripts/ram_baseline.tsv`)做 diff,任何对象 RW+ZI 增量 >1KB 即高亮 #### 0.4 (可选)运行时间统计 @@ -333,7 +347,7 @@ void vApplicationMallocFailedHook(void) - 帧宽榜直接点名宽度大户(自身帧):`s_append` 704 B(FIL 512 + 记录 64 + 时间戳)、`StartDefaultTask` 560 B(探针 `FIL __chk`)、`cfg_store_read`/`cfg_store_write_atomic` 576 B、LVGL 绘制 624/560/400 B——与 §1.4 的整改项对得上。**lftpd 三项已于 2026-09-21 静态化(§1.4),已退出帧宽榜前 15**(原 `handle_control_channel` 1104 B、`receive_file`/`send_file` 1088 B、`send_response` 800 B)。 - **静态分析的天然盲区**:递归/互递归(LVGL widget 树、`layout_update_core`、`apply_theme` 等)与间接调用只能按 "Cycle" 近似,armlink 明确标注 `+ Unknown(Cycles, Untraceable Function Pointers)`。因此 uiTask 的 912 B 是**下限**(实测 3824 B),必须靠运行时水位。 - testTask 在产品构建里只是 `osDelay` 空循环(168 B);2048 words 是给**存储套件构建**(`TEST_SUITE_STORAGE`)预留的,测套件时应重跑本脚本看该构建的静态值。 -- **测 ftpTask 的复现配方**:设备就绪后跑 `& '.venv\Scripts\python.exe' test/host/ftp_speed_test.py --size 128`(STOR 128KB + RETR + 逐字节比对),再按上面命令叠加串口日志出报告。**不要用裸 TCP 工具探测 21 端口**——CH395F 单客户端复用,探测会占掉唯一会话(见 FTP 指南 §8.9)。 +- **测 ftpTask 的复现配方**:设备就绪后跑 `& '.venv\Scripts\python.exe' test/host/ftp_speed_test.py --size 128`(STOR 128KB + RETR + 逐字节比对),再按上面命令叠加串口日志出报告。**不要用裸 TCP 工具探测 21 端口**——CH395F 单客户端复用,探测会占掉唯一会话(见 `docs/测试说明/FTP_Test_Guide.md` §8.9)。 #### 2026-10-10 复测:**不抬栈**把 defaultTask 从 FAIL 拉回 PASS(S-1 销项) @@ -405,7 +419,7 @@ FreeRTOS 堆不变(`free=17248 / min_ever=9312`)。验证链:`@build` 0/0 依据 RM0090 §3.1.1:F407 的 64KB CCM @0x10000000 仅 CPU 可访问(DMA 不可达、无 cache)。 -**进度(2026-09-21)**:第一阶段已落地——scatter 新增 `RW_CCM` 区(`sct:25`,注释 "FreeRTOS heap only")并把 **FreeRTOS 堆整体迁入**(49,184 B,CCM 占用 75.3%)。下表缓冲迁入清单为**后续剩余项**(LVGL 池/UI 缓冲现仍在主 SRAM);`APP_CCM_ENABLE` 门禁宏**尚未实现**(scatter 无开关,见下"风险门禁")。 +**进度(2026-09-21)**:第一阶段已落地——scatter 新增 `RW_CCM` 区(`sct:25`,注释 "FreeRTOS heap only")并把 **FreeRTOS 堆整体迁入**(当时历史快照:49,184 B,CCM 占用 75.3%;现值见 §2.1——2026-10-12 B5 堆已扩至 56 KB)。下表缓冲迁入清单为**后续剩余项**(LVGL 池/UI 缓冲现仍在主 SRAM);`APP_CCM_ENABLE` 门禁宏**尚未实现**(scatter 无开关,见下"风险门禁")。 **迁入 CCM 的缓冲(全部纯 CPU 读写):** @@ -499,9 +513,9 @@ ISR 余量:该任务运行期间可能被中断打断的最深中断链(SPI/ ### 阶段 3:长效机制(0.5 天 + 长期) -1. **RAM 预算台账 `[计划]`**(模板见 §8.2;落地文件 `docs/内存管理/RAM预算台账.md` **尚未创建**):记录每个 Region/缓冲/栈的计划值、实测值、变更历史;每次改内存相关代码必须更新 +1. **RAM 预算台账**(✅ **2026-10-12 落地** `docs/内存管理/RAM预算台账.md`,模板见 §8.2):记录每个 Region/缓冲/栈的计划值、实测值、变更历史;每次改内存相关代码必须更新 2. **AGENTS.md 增补**(✅ 2026-09-21 已完成:"内存预算紧张"条目已含 `ram_report.py`/`stack_report.py` 数据源与"改栈前后各跑一次"要求) -3. **pre-commit 门禁**(可选):钩子解析最新 map,RW_IRAM1 使用率 >92% 或 RW_CCM 超限 → 提交拦截并提示 +3. **pre-commit 门禁**(✅ **2026-10-12 落地** `mem-budget` 钩子 = `scripts/ram_baseline.py --gate`):钩子解析最新 map,RW_IRAM1 使用率 >92% 或 RW_CCM 超限 → 提交拦截并附 Top-10 RW+ZI 对象表 4. **@test 扩展**:`verify_log.py` 增加可选 Required 标记 `"[MEM] heap free"`,把堆余量下限纳入自动化回归 5. **map diff 常态化**:每次 `@build` 后跑 `ram_report.py`,ZI 增量 >1KB 的改动必须在提交说明中解释 @@ -511,7 +525,7 @@ ISR 余量:该任务运行期间可能被中断打断的最深中断链(SPI/ |---|---|---| | 主 SRAM 静态占用 | 87,040 B / 128KB(66.4%) | **~27KB / 128KB**(LVGL 池 + UI 缓冲迁 CCM、`ui_net_sink` 关) | | 主 SRAM 余量 | 44,032 B(43.0KB) | **~101KB** | -| CCM 使用 | 49,184 B / 64KB(75.0%,FreeRTOS 堆) | ~56KB / 64KB(堆 32K + 池 16K + UI 缓冲 + BBT) | +| CCM 使用 | 49,184 B / 64KB(75.0%,FreeRTOS 堆,历史当时值。2026-10-12 B5 后堆 56 KB / 87.9%) | ~56KB / 64KB(堆 32K + 池 16K + UI 缓冲 + BBT) | | 任务栈合计 | 38,912 B(38.0KB) | ~21.5KB(§1.3 修正目标) | | FreeRTOS 堆 | 49,152 B(`free=8,784 / min_ever=4,848` B) | 32,768 B(余 ≥8KB,全负载验证) | | **总余量** | **~68KB**(47.9 + 16.0 + 4.8) | **~120KB** | @@ -570,6 +584,9 @@ ISR 余量:该任务运行期间可能被中断打断的最深中断链(SPI/ | C 库堆/栈 | 512B/1KB | | 仅启动期;lftpd 不得再用 | | ``` +> ✅ 本模板已于 **2026-10-12 落地**为 `docs/内存管理/RAM预算台账.md`(扩展了子系统配额口径、 +> CCM 分配台账与 92% 门禁说明;区域数字由受管区自动生成,不手抄)。 + ### 8.3 关键配置位置速查 | 配置 | 位置 | diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md new file mode 100644 index 00000000..f4683a58 --- /dev/null +++ b/docs/内存管理/RAM预算台账.md @@ -0,0 +1,96 @@ +# RAM 预算台账(配额 · 趋势 · 门禁) + +> **定位**:`docs/内存管理/RAM使用分析与优化方案.md` §8.2 模板与 +> `docs/问题记录/项目审查与优化建议报告.md` §5.5 三件套的**落地件**(2026-10-12,登记簿 N-27 前半 / W1-0)。 +> 回答三个问题:**每个子系统还能用多少 RAM**(配额)、**总量趋势往哪走**(基线 TSV)、 +> **超了怎么拦**(92% 门禁)。 +> +> **数字不手抄**:下方受管区由 `scripts/ram_baseline.py --write` 从 `.map` 生成, +> pre-commit `mem-budget` 钩子校验一致性(同 `mem-numbers` 的"事实源唯一"思路)。 +> 配额是**规划值**(`docs/问题记录/项目审查与优化建议报告.md` §5.5),改动 = 先登记本文件 §6 变更记录,再改 `ram_baseline.py` 的 `QUOTAS`。 +> 区域放置与 DMA 可达性规则 → `内存内容分配策略.md`(本文件不重复)。 + +## 1. 区域预算与门禁(受管区) + + +| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | +|---|---|---|---|---| +| ER_IROM1 (Flash) | 223,724 B | 1,048,576 B | 21.3% | 参考 | +| RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | +| UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | +| RW_IRAM1 | 96,144 B | 131,072 B | 73.4% | PASS | + +| 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | +|---|---|---|---| +| 网络 | 13,410 B | 12 KB | ⚠ 超 1.1 KB | +| 存储 | 7,056 B | 12 KB | OK | +| UI | 54,988 B | 16 KB | ⚠ 超 37.7 KB | +| RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | +| 应用 | 13,119 B | 8 KB | ⚠ 超 4.8 KB | +| 其它 | 7,596 B | — | 参考 | + + +- **门禁**:pre-commit `mem-budget` 钩子(`scripts/ram_baseline.py --gate`)—— + RW_IRAM1 / RW_CCM 任一 >92% 即拦提交,并打印 Top-10 RW+ZI 对象定位增量; + 无 `.map` 时自适应跳过(CI 盲区登记在 `docs/任务规划/测试与架构专项.md` A-5 残余)。 +- **阈值依据**:92% = `docs/问题记录/项目审查与优化建议报告.md` §5.5 第 1 条——留 ~8% 给突发缓冲与栈抬升, + 触线即触发排期,而不是静默逼近溢出。 +- **软提醒**(不拦提交):tsv 末行落后于 `.map`(忘跑 `--update`)、`.map` 早于最新源码(忘 `@build`)。 +- **子系统归集为参考值**:按对象名前缀归集(口径见 §2),合计与区域总量可差几十字节 + (链接器填充行与 `UNINIT_CCM` 等小区分段所致);区域行才是门禁事实源。 + +## 2. 子系统配额口径 + +配额值(`ram_baseline.py` 的 `QUOTAS`,与 `docs/问题记录/项目审查与优化建议报告.md` §5.5 第 3 条一致; +RTOS 堆 2026-10-12 B5 扩容 48→56 KB 见 §6):**网络 12 KB / 存储 12 KB / +UI 16 KB / RTOS 堆 56 KB / 应用 8 KB**。归集口径(按 `.map` 对象名,首匹配): + +| 子系统 | 计入对象(前缀/精确名) | 不计入 | +|---|---|---| +| 网络 | `ch395f*` `net_*` `lftpd*` `ftp_task*` | `ui_net_sink`(算 UI) | +| 存储 | `gd5f*` `nand_*` `ff*` `diskio*` `vol_lock*` + dhara 裸名(`map/journal/nand/fctl/era.o`) | `cfg_*`(算应用) | +| UI | `lv_*` `ui_*`(含 `ui_net_sink`、`ui_task`) | — | +| RTOS 堆 | `heap_4.o`(CCM)——配额 = `configTOTAL_HEAP_SIZE` 本身 | 内核对象(`tasks/queue/timers/...` 归"其它") | +| 应用 | `App/` 其余:采集 `adc_*`、保护 `prot_*`、相量 `phasor/sample_ring`、配置 `cfg_*`、日志 `sys_/dbg_/console_log`、对时 `time_sync*`、`sys_clock`、`task_watchdog`、`sha256`、`tpafe*`、`sd2506`、`default_task` 等 | — | +| 其它 | 内核对象、HAL、CMSIS、启动文件、C 库、链接器填充 | — | + +**现状判读(2026-10-12 建档)**: + +- **UI ⚠ 超配额 = W3 的既有债务,不是新增违规**:LVGL 池 40 KB(R-9 目标 16 KB)+ 帧/远显缓冲 + ≈12.8 KB(R-2 目标迁 CCM)——配额 16 KB 描述的是 **R-9/R-2 落地后的目标态**(排期见 + `docs/任务规划/README.md` §2 W3)。 +- **RTOS 堆 = 配额本身**:56 KB 即 `configTOTAL_HEAP_SIZE`(2026-10-12 B5 由 48 KB 扩容,见 §6; + `heap_4.o` 另含 32 B 元数据,不计超);**改堆 = 改配额**,必须先过本文件 §6 登记。 +- **应用 ⚠ 小幅超**:8 KB 为报告规划值;2026-10-10 栈整改把 3 处 `FIL` 从栈搬进 BSS + (静态 +4,128 B,见 `docs/内存管理/RAM使用分析与优化方案.md` §4"2026-10-10 复测")—— + 属已评审的"以空间换栈安全",差额在此如实体现。 + +## 3. CCM 分配台账(谁能用 CCM) + +| 项 | 值 | 说明 | +|---|---|---| +| CCM 容量 | `RW_CCM` Max 见受管区 | scatter `RW_CCM`;**仅 CPU 可达,DMA 不可达**(RM0090 §3.1.1) | +| 现占用 | 仅 FreeRTOS 堆 `heap_4`(56 KB,2026-09-21 迁入;2026-10-12 B5 扩容 48→56 KB) | 其余候选全部仍在 SRAM1 | +| 现余量 | 受管区 RW_CCM 容量 − 占用(≈8 KB) | **不够** R-2 后半的 UI 侧待迁体量(R-9 后仍 ≈30 KB) | +| R-2 后半解锁链 | ① R-9:`LV_MEM_SIZE` 40→16 KB(待迁体量降到 ~30 KB)→ ② 堆 56→32 KB(`docs/内存管理/RAM使用分析与优化方案.md` §2.2,再腾 24 KB;⚠ 2026-10-12 B5 反向扩容至 56 KB——9 任务栈 + 队列实测需要,缩回 32 KB 前必须重跑 `stack_report.py` + 上板 min_ever 复测)→ ③ CCM 余 ~32 KB ≥ 30 KB → 迁入 | **顺序不可颠倒**;每步前后跑 `stack_report.py` / `ram_report.py`(铁律) | +| 迁入前置 | 目标缓冲必须过 DMA 可达性清单(`内存内容分配策略.md`) | 违例 = 静默传错(N-11 同类坑);克隆芯片 CCM 可靠性风险见 `docs/内存管理/RAM使用分析与优化方案.md` §7 | + +## 4. 趋势基线(`scripts/ram_baseline.tsv`) + +- **每次 `@build` 后**:`& '.venv\Scripts\python.exe' scripts/ram_baseline.py --update` + 追加一行(Code/RO/RW/ZI + 三区水位 + Top-10 RW+ZI 对象;同日重跑覆盖当日行)。 +- **看趋势 / 找增量**:`scripts/ram_baseline.py --trend`——高亮较上一行 RW+ZI 增长 >1 KB 的对象, + 即 `docs/内存管理/RAM使用分析与优化方案.md` §0.3 "ZI 增量 >1KB 必须在提交说明中解释"的机械化;提交说明直接抄 `--trend` 输出行。 + +## 5. 超配额流程 + +新增/扩大静态缓冲导致子系统越过配额:**先登记再动手**——本文件 §6 加一行 +(日期 / 子系统 / 现值→新值 / 原因 / 决定人),提交说明引用该行。 +92% 门禁拦的是**总量**,配额管的是**结构**(谁挤占了谁),两者不可互替。 + +## 6. 变更记录 + +| 日期 | 变更 | 原因 / 出处 | +|---|---|---| +| 2026-10-12 | 建档:三件套落地(本台账 + `scripts/ram_baseline.py` + `scripts/ram_baseline.tsv` + pre-commit `mem-budget` 钩子) | 登记簿 N-27 前半(W1-0);`docs/问题记录/项目审查与优化建议报告.md` §5.5 | +| 2026-10-12 | **RTOS 堆配额 48 KB → 56 KB**(`QUOTAS` 同步) | B5 protectTask 立项:iec104Task(2.5KB)+protectTask(2KB)+TCB 入堆后 48KB 池 min_ever 预估跌破 3,072 B 告警线 → `.ioc` L1 参数 `configTOTAL_HEAP_SIZE` 49152→57344(CCM 水位 75.3%→87.9%,仍 <92% 门禁;CCM 空闲降至 ≈8 KB)。连带影响见 §3 R-2 后半链注记 | diff --git a/docs/内存管理/内存内容分配策略.md b/docs/内存管理/内存内容分配策略.md index 42d7ae71..c57bb571 100644 --- a/docs/内存管理/内存内容分配策略.md +++ b/docs/内存管理/内存内容分配策略.md @@ -24,7 +24,7 @@ 3. CCM 存在性已用 `ST-LINK_CLI -r32 0x10000000` 本机实测可读写;**克隆片可能缺 SRAM3,换料必须重测并回退布局**。 4. 工程不进入 Stop/Standby 低功耗模式,两区域掉电保留行为无差异,不构成分配判据。 -> 勘误(原《STM32F4-Base 内存使用分析报告》2026-09-11 快照,随主文档合并保留于此):该报告内存地图中 "SRAM3 64KB (0x2001_C000)" 为笔误——F407 的 SRAM3 即 CCM,位于 `0x10000000`(AXI);AHB2 侧只有 SRAM1(112KB)+SRAM2(16KB)。**以本文为准**(主文档 §8.4 与 `内存布局_RAM.svg` 均按此口径)。 +> 勘误(原《STM32F4-Base 内存使用分析报告》2026-09-11 快照,随主文档合并保留于此):该报告内存地图中 "SRAM3 64KB (0x2001_C000)" 为笔误——F407 的 SRAM3 即 CCM,位于 `0x10000000`(AXI);AHB2 侧只有 SRAM1(112KB)+SRAM2(16KB)。**以本文为准**(`RAM使用分析与优化方案.md` §8.4 与 `内存布局_RAM.svg` 均按此口径)。 ## 3. 全板 DMA 使用方清点(DMA 缓冲必须为 SRAM 静态) @@ -38,7 +38,7 @@ ## 4. 内容分配规则 -- **R1(CCM 驻留)**:CCM 只放"**CPU-only 且损坏即致命、无自愈**"的内存。当前唯一驻留者 = **FreeRTOS 堆**(`heap_4.o`,48KB:全部任务栈/队列/定时器)。scatter 的 RW_CCM 节**不得**再列其他对象;任何新增必须经 §5 式裁决并走 §7 完整验证阶梯。 +- **R1(CCM 驻留)**:CCM 只放"**CPU-only 且损坏即致命、无自愈**"的内存。当前唯一驻留者 = **FreeRTOS 堆**(`heap_4.o`,56KB——2026-10-12 B5 由 48KB 扩容,`.ioc` L1 参数 `configTOTAL_HEAP_SIZE=57344`;全部任务栈/队列/定时器)。scatter 的 RW_CCM 节**不得**再列其他对象;任何新增必须经 §5 式裁决并走 §7 完整验证阶梯。 - **R2(DMA 缓冲)**:只允许文件作用域静态缓冲,必须落 SRAM。两条防线缺一不可: - **尺寸防线**:GD5F DMA 阈值 `GD5F_DMA_THRESHOLD=1024`(>1024B 才走 DMA;阈值以下一律 CPU 轮询)。取值须 ≥ 存储路径上可能出现在任务栈的最大缓冲(dhara meta 132B、`dhara_nand_is_free` 64B)、且 < 页大小(2048,保住页级热路径的 DMA)。 - **地址防线(2026-09-21 加固)**:`gd5f_dma_buf_ok()` 运行时校验**整段**缓冲是否落在 SRAM 区间(0x20000000~0x2001FFFF),越界者拒走 DMA、回落 CPU 轮询,并打印 `[ERR][NAND] RX/TX DMA buffer not DMA-reachable (p=… size=…) fallback poll`。 @@ -71,7 +71,7 @@ CCM 64KB 只能容纳 {FreeRTOS 堆 48KB} 与 {LVGL 集合 ~52.5KB} 之一(两 当时提出的两条判据经核实**均为事实但不具区分力**: - "UI 不用 DMA" ✓ 属实(§3 路径核实:DMA 只寻址驱动静态缓冲)→ 堆同样不用 DMA,两候选在 DMA 轴**等价** -- "图形不紧迫速度" ✗ 无效判据(§2.1:M4 无 cache,CCM 与主 SRAM 对 CPU 同速)→ 两候选在速度轴**等价** +- "图形不紧迫速度" ✗ 无效判据(§2:M4 无 cache,CCM 与主 SRAM 对 CPU 同速)→ 两候选在速度轴**等价** 真正可判别的轴是**"该内存损坏后的后果" × "DMA 不可达带来的隔离收益"**: @@ -108,12 +108,12 @@ CCM 64KB 只能容纳 {FreeRTOS 堆 48KB} 与 {LVGL 集合 ~52.5KB} 之一(两 触发条件:测试构建 SRAM 余量不足,**且**已耗尽 `RAM使用分析与优化方案.md` 的 R-4 帧副本去重(−6.4~9.6KB)与 LVGL 池缩减等低代价手段。 流程:改 scatter → 走 §7 完整验证阶梯 → 同步更新本文档、`AGENTS.md`、`.sct` 头注释。在此之前,方案 A 是唯一有效布局。 -## 6. 当前容量台账(2026-09-21,最坏构建 = 存储套件) +## 6. 当前容量台账(2026-10-12,产品构建;2026-09-21 最坏构建=存储套件的历史基线见 `docs/内存管理/RAM预算台账.md` 趋势) | 区域 | 总量 | 已用 | 空闲 | 驻留内容 | |---|---|---|---|---| -| RW_IRAM1(SRAM) | 131,072B (128KB) | 78,188B (~76.4KB) | 52,884B (~51.7KB) | 全部静态(除堆),含 LVGL 集合 53,805B | -| RW_CCM | 65,536B (64KB) | 49,152B (48KB) | 16,384B (16KB) | **仅** `heap_4.o`(ucHeap 48KB) | +| RW_IRAM1(SRAM) | 131,072B (128KB) | 95,816B (~93.6KB, 73.1%) | 35,256B (~34.4KB) | 全部静态(除堆),含 LVGL 集合 53,805B | +| RW_CCM | 65,536B (64KB) | 57,376B (56KB, 87.9%) | 8,160B (~8KB) | **仅** `heap_4.o`(ucHeap 56KB,2026-10-12 B5 扩容) | LVGL 集合明细(均在 SRAM):LVGL 池 40,960B(`lv_mem_core_builtin.o`,`LV_MEM_ADR=0` 内部静态)+ `g_ui_draw_buf` 3,232B(3200+32 保护带)+ `g_ui_fbuf` 3,200B + `g_tx` 3,209B + `g_lcdmem_data` 3,204B。 @@ -148,4 +148,4 @@ LVGL 集合明细(均在 SRAM):LVGL 池 40,960B(`lv_mem_core_builtin.o` 已知未决风险: - `.ioc` 未与 `freertos.c` 的栈值直改同步——CubeMX 重新生成会覆盖栈值(已注释标记,未修) -- CCM 存在性依赖芯片批次(克隆片可能缺 SRAM3,见 §2.3) +- CCM 存在性依赖芯片批次(克隆片可能缺 SRAM3,见 §2) diff --git a/docs/内存管理/存储栈RAM优化调研.md b/docs/内存管理/存储栈RAM优化调研.md index d0eaf355..5d436889 100644 --- a/docs/内存管理/存储栈RAM优化调研.md +++ b/docs/内存管理/存储栈RAM优化调研.md @@ -2,7 +2,7 @@ > 版本 v1.1 日期 2026-09-21 作者 AI 辅助调研 > 数据来源:`data/ram_report_20260921.md`(.map 静态占用)+ 逐行代码核验(`nand_ftl.c` / `gd5f2gq5ue.c` / `Lib/dhara` / `Lib/FatFs/ffconf.h` / `lftpd*`) -> 定位:`RAM使用分析与优化方案.md` 的存储专项补充;优化项编号 P0/P1/P2,与总方案 §6 的 S 序号在"规划"一节对齐 +> 定位:`RAM使用分析与优化方案.md` 的存储专项补充;优化项编号 P0/P1/P2,与 `docs/内存管理/RAM使用分析与优化方案.md` §6 的 S 序号在"规划"一节对齐 > v1.1 清理(2026-09-21 同日):移除已落地的 P0-1 BBT 页缓冲收缩(−1,776B,commit `be1a30a`)与编程回读校验整体移除的实施记录(详见 GD5F Trap 05 / 错误码传播审计),以及过期逐模块基线表;仅保留未实施项与回归要求 ## 1. 现状约束(不变,是下述全部方案的依据) @@ -28,7 +28,7 @@ **明确不做**:移除 dhara(丢掉电一致性与坏块重映射,代价不对称);journal checkpoint 分片(违反 dhara 协议);调 `gc_ratio`(无 RAM 收益);`FF_FS_REENTRANT=0`(defaultTask+ftpTask 并发访问 FatFs 必需)。 -## 3. 分阶段规划(与总方案 §6 S 序号对齐) +## 3. 分阶段规划(与 `RAM使用分析与优化方案.md` §6 S 序号对齐) | 阶段 | 内容 | 收益 | 前置 | |---|---|---:|---| diff --git a/docs/子系统设计/ADC采样节拍设计说明.md b/docs/子系统设计/ADC采样节拍设计说明.md index cf802f90..fbf38a59 100644 --- a/docs/子系统设计/ADC采样节拍设计说明.md +++ b/docs/子系统设计/ADC采样节拍设计说明.md @@ -4,6 +4,11 @@ > (`Src/main.c` 回调 + `app_main.c` 启动 + `adc_task.c` 退化为纯消费者),`@build` 0 错 0 警; > 台架 `@test` **PASS**(`frame_seq` 每 10 s 精确 +10000 = 1000.0 Hz、`frst_err=0`、回读一致; > 实测见 §7.1)。 +> **事件驱动消费改造方案 a 已落地(Task #32 P1-5,2026-10-10)**:用户决策 +> "ISR 只发唤醒 + 计算留任务 + 提优先级"(方案 a),拒绝"ISR 内计算"。消费端已从 +> 1 ms `osDelayUntil` 轮询改为 **TIM6 ISR 置位 `ADC_WAKE_FLAG` 唤醒**,`adcTask` +> 优先级 `Normal` → **`AboveNormal`**。台架实测 **`idle = 0`**(20000 拍全零;改前 1~649)、 +> `frames` 严格 1000.0 Hz、UI 侧 `uip/uiw/uis` 无退化。详见 §10。 > **上位需求**:`docs/算法/继电保护功能复现规划.md` §3.2 L1-1(周期 CONVST 重触发 1000 Hz, > 与保护节拍同源)。**驱动契约**:`Drivers/BSP/TPAFE5160/README.md`。 > **背景**:B1(2026-10-10)已把 `adc_task` 由空桩改为真实消费者,但采样节拍是 @@ -126,7 +131,7 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) ### 4.5 文档同步(2026-10-10 已执行) - `docs/算法/继电保护功能复现规划.md`:§3.2 L1-1 由 🔶 改为 ✅(TIM6 硬件触发)、§1.2 采样软件行 + 变更记录同步。 -- `docs/待处理内容.md` §4:TPAFE 自动扫描模式待确认项标注"已不影响"(TIM6 方案不依赖它)。 +- `docs/任务规划/基线与待确认.md` §1:TPAFE 自动扫描模式待确认项标注"已不影响"(TIM6 方案不依赖它)。 - `AGENTS.md` 架构要点 `adcTask` 条 + `app_main_init` 序列:改为"TIM6 硬件触发 + 纯消费者"。 - `Drivers/BSP/TPAFE5160/README.md`:周期重触发改为"由 TIM6 更新中断提供"。 @@ -167,7 +172,8 @@ t=1000 µs 下一个 TIM6 更新事件 跑 `python scripts/check_mem_numbers.py --write` 刷新快照。 7. **消费端与生产端同频**:TIM6(84 MHz 派生)与 FreeRTOS tick TIM7(同源)都恰好 1 kHz, 相位固定 → 消费端每拍恰好取到 1 帧。**一旦进入等周波调频(采样率≠1 kHz), - 1 ms 轮询会与生产端失配 → 消费端须改为"帧就绪事件驱动"**(见 §8)。 + 固定周期轮询会与生产端失配 → 消费端须改为"节拍事件驱动"**(见 §8)。 + ✅ **该前置已满足(2026-10-10,§10)**:消费唤醒改由 TIM6 ISR 置位,与采样率天然同步。 --- @@ -197,11 +203,15 @@ t=1000 µs 下一个 TIM6 更新事件 | 6 | 开机时间戳正常 | ✅ 日志带墙钟时间戳,`net init: OK` 在 +530 ms | | 7 | 无 HardFault / `[ERR]` / 看门狗复位 | ✅ 零 `[ERR]`/HardFault;`reset cause:` 仅 1 次(调试器软复位) | -**关于 `idle`(不是缺陷,是轮询消费的固有抖动)**:生产端(TIM6)与消费端(`adcTask` 1 ms 轮询) +**关于 `idle`(改前:轮询消费的固有抖动)**:生产端(TIM6)与消费端(`adcTask` 1 ms 轮询) 同为 1000 Hz(TIM6 与 FreeRTOS tick 源 TIM7 参数一致,均 84 MHz/84/1000),但二者无显式同步 → 偶发 ±1 帧的相位抖动:每 10 s 约 5 次"两次轮询落在同一生产间隔"(`idle` +5)配 5 次"一次轮询跨两个 -生产间隔"(`frames` 少 5,即 9995/10000 ≈ 99.95% 取数)。要彻底消除需把消费端改为**帧就绪事件驱动** -(在 BUSY EXTI 读帧完成时通知 `adcTask`)——与 §8 等周波调频的消费端改造是同一件事,留待该阶段一并做。 +生产间隔"(`frames` 少 5,即 9995/10000 ≈ 99.95% 取数)。 + +✅ **现状(§10 已落地,2026-10-10)**:消费端改由 **TIM6 ISR 置位 `ADC_WAKE_FLAG`** 唤醒。 +落点与当初设想的"BUSY EXTI 读帧完成时通知"不同——选 TIM6 是为了让唤醒与**采样节拍**严格同源, +且不必把内核 API 引入 BUSY EXTI(它与 TIM6 同抢占优先级,见 §5);效果一致:实测 **`idle = 0`** +(6 次上板、每次 20000 拍全零)→ 判据 5 由"基本为 0"收紧为**恒 0**。 --- @@ -213,7 +223,8 @@ t=1000 µs 下一个 TIM6 更新事件 - 本配置已为此预留:**1 µs 分辨率** + **ARR 预装载使能** → 运行期 `__HAL_TIM_SET_AUTORELOAD(&htim6, 周期µs - 1)` 即可无毛刺改频。 - 需要配套:① 测频源(过零/DFT 相位);② 把当前采样率同步给保护算法(相量/定值); - ③ **消费端改事件驱动**(§6.7)——否则 1 ms 轮询在非 1 kHz 采样率下会丢帧。 + ③ **消费端改事件驱动**(§10)——否则固定周期轮询在非 1 kHz 采样率下会丢帧。 + ✅ **已落地(2026-10-10,§10)**:消费唤醒与 TIM6 同源 → 调频时只改 ARR,消费节拍自动跟随。 --- @@ -224,3 +235,234 @@ t=1000 µs 下一个 TIM6 更新事件 | 2026-10-10 | 建立本文档 | B1(ADC 采集链路)遗留项"精确 1 kHz 硬件定时器触发"的规划:方案选型(A/B)、CubeMX 配置规格、代码改造清单、时序与优先级分析、风险与验收判据、等周波调频预留 | | 2026-10-10 | **实现(方案 A)** | CubeMX 加 TIM6(PSC=83 / ARR=999 / ARR 预装载 / NVIC 抢占优先级 6);`Src/main.c` 回调加 TIM6 分支触发 CONVST;`app_main.c` 用 `HAL_TIM_Base_Start_IT(&htim6)` 启动节拍;`adc_task.c` 退化为纯消费者。`@build` 0 错 0 警;台架 `@test` 验收待做 | | 2026-10-10 | **台架 `@test` 验收 PASS** | 真机闭环:`seq` 10005→20005→30005(每 10 s +10000 = 1000.0 Hz)、`frst_err=0`、`idle` 每 10 s +5(0.05%,轮询抖动)、回读 `No difference found`、零 `[ERR]`/HardFault、`reset cause` 仅 1 次。证据 `boot_log_data/20261010_prod_adc_tim6_boot_log.txt` | +| 2026-10-10 | **事件驱动消费方案 a 规划入档(Task #32 P1-5)** | 记录用户决策与改造清单(TIM6 ISR 置位 `ADC_WAKE_FLAG` 唤醒 `adcTask` + `adcTask` 提至 `osPriorityAboveNormal`),并附下一版硬件 DMA 双缓冲建议(§10.5)。代码待实施,消费端仍为 1 ms 轮询 | +| 2026-10-10 | **事件驱动消费方案 a 已实施(Task #32 P1-5)** | 四处改动落地:`Src/main.c` TIM6 分支调 `adc_tick_irq_notify()`;`adc_task.c` 循环尾改 `osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever)` 并删 `ADC_TASK_BEAT_MS`/`next`;`Src/freertos.c` 优先级 `Normal`→**`AboveNormal`**;`App/task/adc_task.h` 加 `ADC_WAKE_FLAG` 与 ISR 入口。实施时补**就绪门**(§10.6,规划未预见:TIM6 比调度器早起 ≈100 ms)。`@build` 0 错 0 警(Code 256368→256728);`@test` PASS:**`idle = 0`/19999 拍**(改前 1~649)、`frames` 1000.0 Hz、`seq−frames = 7` 正是就绪窗口、UI 侧 `uip/uiw/uis` 无退化 | +| 2026-10-10 | **§10.1/§10.3/§10.4 复核修正(无功能改动)** | ① 2 ms 忙等的单拍上界由"9 通道 ×2 = 18 ms"更正为**一次超时 ≈2~3 ms**(`stm32_scan()` 在首个超时处即 `Stop`+`return`);② 6 次上板对照 `adcp` = **122 µs**(超时 0 次的 5 轮)/ **2209 µs**(超时 1 次的那轮)→ 2 ms 不是常态开销;③ `adcm` 2.1 ms 峰值**归因改判**为每 10 s 一次的心跳日志(`idle=0`/20000 反证无每拍 2 ms);④ 新增超时的**正确性**代价(超时拍 STM32 侧静默沿用上一拍值,`skip` 抓不到)与硬化排序 | +| 2026-10-10 | **补 ADC1 上电自校准(§10.4)** | 发现 F4 必需的自校准在本项目**完全缺失**(CubeMX 不生成 + 裁剪 HAL 无 `HAL_ADCEx_Calibration_Start` + 裁剪 CMSIS 头无 `ADC_CR2_CAL`)→ 在 `Src/adc.c` 的 `USER CODE` 区实现 `adc1_calibrate()`(RM0090 §13.3.6 时序),`MX_ADC1_Init()` 末尾调用。新增审计项 `adc1-calibration-call`/`-impl`。`@build` 0 错 0 警(Code 256728→256868,+140 B);`@test` PASS,`adcp` 仍 122 µs、`adc1_err=0`;实测 STM32 侧 6 路同向偏移 +10~+63 LSB、TPAFE 侧不变 | +| 2026-10-10 | **修"失败拍沿用旧值"(§10.4 硬化 ①)** | `stm32_scan()` 两条失败支路都清 `s_stm32_valid` → 失败拍走既有 `skip` 整帧跳过,不再产出"半新半旧"混合帧污染相量窗;下一拍成功自愈。主机单测 70 → **79** 用例(新增 TC-AI-070..078),并做反向验证(改回旧行为 3 条立即 FAIL)。`@build` 0 错 0 警(Code 256868→256872)、`@test` PASS。硬化 ②(DMA 消忙等)仍未做,并更正原表述:**F4 的 ADC EOC 没有 EXTI 通道**,只有 `ADC1_2_IRQn` | +| 2026-10-10 | **v2.8:节拍源 TIM6→TIM3 + STM32 侧 DMA 乒乓双缓冲(§11)** | 用户选定最彻底方案(DMA + TRGO 硬件触发 + 消费上一拍半区)。更正一处硬事实:**F4 上 ADC1 无 TIM6/TIM7 TRGO**(只接 DAC)→ 节拍源改 **TIM3**(PSC 83/ARR 999,`TIM_TRGO_UPDATE`),TIM6 从 `.ioc` 整体移除;ADC1 挂 **DMA2_Stream4**(CubeMX 默认抢 Stream0 会连锁搬走 SPI1_RX/USART1_RX,且换流把 NVIC 优先级重置为 **0**,违反"外设中断 ≥5")、`DMAContinuousRequests=ENABLE`、循环模式长度 2×9 乒乓,半区由 `seq` 奇偶推出(只读一个 32 位量 → 无竞态)。`stm32_scan()` → `stm32_take()`,逐通道 `PollForConversion` 全删;故障计数拆成 `adc1_seq`/`adc1_gap`/`adc1_err`/`adc1_hw` 四类(单拍无新帧 = 相位伪影,不计故障)。实测:`adcp` **122 µs → 8 µs**、`adc1_err=0`、`adc1_hw=0`、`adc1_gap=27`/20002 拍、`adc1_seq` 与 `seq` 恒定偏移 2;主机单测 79 → **101**(四组反向验证均咬);`@build` 0 错 0 警 Code 256872→**257660**;`@test` PASS;adcTask 静态链 832 B / 余 59% 不变 | + +--- + +## 10. 事件驱动消费改造(Task #32 P1-5,2026-10-10:用户决策方案 a,**已落地**) + +> 上位背景:Task #32 暴露"UI 任务看门狗停摆"候选占用者之一是 `adcTask` 的 1 ms 轮询—— +> 原 `osDelayUntil(1ms)` 把消费唤醒**钉在 FreeRTOS tick(1 ms)边界**,带来 0~1 ms 随机 +> 相位抖动 + 累积漂移风险,且 `adcTask` 与 `uiTask` 同为可被长时抢占的对象。用户决策: +> **方案 a(ISR 只发唤醒 + 计算仍在任务 + 提优先级)**,并拒绝"ISR 内计算"方案。 +> **落地状态(2026-10-10)**:四处改动全部实施(清单见 §10.2),`@build` 0 错 0 警、 +> 台架 `@test` PASS,实测见 §10.3;实施时补了一条规划未预见的**就绪门**(§10.6)。 + +### 10.1 决策:为什么选 a,不选"ISR 内计算" + +- **拒绝"ISR 内计算"**:把相量/压环搬进 TIM6 ISR 是反模式—— + ① `adc_input_poll()` 内含 `HAL_ADC_PollForConversion` 的 **2 ms 忙等**(`ADC_INPUT_POLL_TIMEOUT_MS=2U`), + 绝不可入 ISR(TIM6 抢占优先级 6,自旋期间低优先级中断与全部任务都被压住;单拍最坏 = + **一次**超时 ≈2~3 ms——`stm32_scan()` 在首个超时处即 `HAL_ADC_Stop()`+`return` + (`App/algo/adc_input.c:79-84`),不会累加成 9 次); + ② 相量/采样环是任务级共享数据,ISR 内改写需额外临界区,破坏 `tpafe5160_snapshot()` 的 + "仅任务上下文"铁律;③ `HAL_TIM_PeriodElapsedCallback` 是 CubeMX 生成区回调,承载 L2 计算 + 违反"生成区不改"约定,重生成即丢。 +- **选定方案 a**:TIM6 ISR **只置位唤醒标志**(`osThreadFlagsSet`),所有计算留在 `adcTask`; + 并把 `adcTask` 优先级由 `osPriorityNormal` 提到 `osPriorityAboveNormal`,使唤醒后**立即抢占** + 其余 Normal 任务,固定 µs 级延迟。计算量、数据共享、生成区约定全部不变。 + +### 10.2 改造内容(已实施) + +| 文件 | 实际改动 | +|---|---| +| `Src/main.c` | `HAL_TIM_PeriodElapsedCallback` 的 TIM6 分支(`USER CODE` 区,L2):`tpafe5160_start_conv_irq()` 后加 **`adc_tick_irq_notify();`**——**不是**直接 `osThreadFlagsSet`(理由见 §10.6);并在 `USER CODE BEGIN Includes` 加 `#include "adc_task.h"` | +| `App/task/adc_task.c` | 主循环尾 `osDelayUntil(next)` → **`(void)osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever);`**;删除 `ADC_TASK_BEAT_MS` 与 `next`(P0-2 的重同步随之不再需要);新增 `adc_tick_irq_notify()` 与就绪门 `s_consumer_ready` | +| `Src/freertos.c` | `adcTask_attributes.priority`:`osPriorityNormal` → **`osPriorityAboveNormal`**(=32 > 其余 Normal=24;`configMAX_PRIORITIES=56` 有裕量) | +| `App/task/adc_task.h` | 新增 `#define ADC_WAKE_FLAG 0x0001U` 与 `void adc_tick_irq_notify(void);`。**未**把 `extern osThreadId_t adcTaskHandle;` 放进头文件——句柄只在 `adc_task.c` 内引用,`main.c` 不碰任何内核符号(与 `task_wd_tick_probe()` 同构;规划里的这一条按此收敛) | + +- **ISR 安全性(设计校验)**:TIM6 抢占优先级 = 6 ≥ `configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY`(=5) → + `osThreadFlagsSet` 在 ISR 内走 `xTaskNotifyFromISR` + `portYIELD_FROM_ISR`(CMSIS-RTOS V2 的 + `osThreadFlagsSet` 实现),合法且会立即触发上下文切换。`configUSE_OS2_THREAD_FLAGS=1` 已开,符号已编入。 +- **看门狗不变**:`TASK_WD_ADC` 心跳仍在循环内每拍调用,`required_mask` 与心跳位集合一致, + 优先级调整不影响掩码。 +- **验证前置**:实施后须 `@build` 0 错 0 警 + 台架 `@test` 复核 `idle` 是否降至 ≈0(§7.1)。 + +### 10.3 收益与语义(含落地实测) + +1. **唤醒延迟固定 µs 级**:原 `osDelayUntil(1ms)` 把唤醒量化到 tick 边界 → 0~1 ms 随机相位抖动 + + 一旦超拍即"永久落后/空转"风险(即 P0-2 的成因);事件驱动走 TIM6 硬件节拍,唤醒 = 生产时刻 + + ISR/调度延迟(亚 µs~数 µs),**无 tick 量化、无累积漂移**。 +2. **idle 归零(实测已达成)**:原轮询与生产的 ±1 帧相位抖动消失——唤醒与生产同源锁定。 +3. **粘滞标志的健壮性**:若单拍偶超 1 ms,TIM6 多发脉冲合并为一次唤醒,`adc_input_poll()` 返回 + `r==0`(无新帧)记 `idle`,**不重复处理同帧**,与 P0 修复(仅 `r==1` 压环)口径一致,无 DFT 窗污染。 + +**落地实测(2026-10-10,`@test` PASS,开机 29 s,台架无客户端)**: + +| 指标 | 改前(1 ms 轮询 + Normal) | **改后(事件驱动 + AboveNormal)** | 判读 | +|---|---|---|---| +| `idle` 无新帧拍数 | 649/20001;P0-1 后仍 **1/20000** | **0 / 19999**(两次心跳全零) | 相位抖动**彻底消失**,唤醒与生产同源 | +| `frames` 速率 | 1000.0/s | **1000.0/s**(t=21155→29227 ms 恰 +8072 拍) | 节拍未丢、未重复消费 | +| `ring` | == `frames` | == `frames`(19999) | P0-1 口径保持 | +| `adcm` 单拍最坏 | ≈2.1 ms | **2.12~2.25 ms** | ⚠ 归因已复核:峰值来自**每 10 s 一次的心跳日志**(`DBG_INFO` + `log_phasors`),不是采集——同批 `adcp` 仅 122 µs,且 `idle=0` 反证没有"每拍 2 ms"(否则粘滞标志会合并唤醒、`idle` 必 >0) | +| `uip` / `uiw` / `uis` | 61.3 / 11.4 ms / 1 µs | **61.3 / 11.4 ms / 1 µs** | 提优先级**未**伤到 UI(LVGL 地板不变) | +| `defd` 控制台排空 | 46 ms | 46 ms | 未被 AboveNormal 挤动 | +| `seq` − `frames` | — | 20006 − 19999 = **7** | 正是"TIM6 已在跑、消费者尚未就绪"的 ≈100 ms 窗口(§10.6),与设计一致 | + +### 10.4 残留风险与后续硬化(落地后仍开放) + +- **Task #32 §4.4 的 2 ms 忙等仍在 L1 层,且如今跑在 AboveNormal 上**:`adc_input_poll()` 内的 + `HAL_ADC_PollForConversion`(`ADC_INPUT_POLL_TIMEOUT_MS=2U`)是任务级忙等(`HAL_GetTick()` + 1 ms 粒度、判据 `> Timeout` → 实际 **2~3 ms**)。提优先级后,STM32 ADC 一旦偶发超时,这 2~3 ms + 会在**高于 net/ftp/default/timeSync** 的优先级上压住它们——比改前更敏感。 + **6 次上板对照(2026-10-10)**:超时 1 次的那轮 `adcp = 2209 µs`;超时 0 次的 5 轮 + `adcp = 122 µs`(= 9 通道 × 7.4 µs + TPAFE 快照)→ **2 ms 只在超时拍出现,不是常态开销**。 + ⚠ **超时的第二个代价是正确性,不只是耗时**:超时拍 `s_stm32_valid` 仍是上次成功留下的 1, + 而 `adc_input_poll()` 的返回值只由 TPAFE 决定 → 该拍照样 `r == 1`、照样压环,STM32 侧几路 + (UB/UC/IB/IC/IN/温度)**静默沿用上一拍的值**(`adc_input.c:209-212`),`s_frames_skip` + 只统计"从未成功过"、抓不到它。 + **硬化排序**:① ✅ **已实施(2026-10-10)**——`stm32_scan()` 两条失败支路(启动失败 / 轮询 + 超时)都清 `s_stm32_valid` → 本拍 STM32 侧判为无有效数据,走既有 `skip` 整帧跳过路径, + 下一拍成功自动恢复;主机侧新增 **TC-AI-070..078** 钉死并做反向验证(改回旧行为 3 条立即 + FAIL)。② ✅ **已实施(2026-10-10)= DMA**:见 **§11**——TIM3 更新事件经 TRGO 硬件触发 + ADC1 一轮 9 通道,`HAL_ADC_Start_DMA(&hadc1, buf, 18)`(乒乓双缓冲)由 DMA 自动搬运, + 任务侧 `HAL_ADC_PollForConversion` **全部删除**,实测单拍 `adcp` **122 µs → 8 µs**; + "超时拍沿用旧值"这条支路在结构上消失(不再有超时概念,改由"本拍无新帧"走同一条 `skip` + 防线)。⚠ **F4 的 ADC EOC 没有 EXTI 通道**,只有 `ADC1_2_IRQn`——"EOC 改 EXTI"在 F4 上 + 不成立,别照 F3/G4 的做法抄。 + ③ 双缓冲已随 ② 同一批落地;**等周波触发**属 V0.2 硬件项(§10.5 / §8)。 +- **已做的一项(2026-10-10):补 F4 ADC 上电自校准**。CubeMX **不生成**该步骤,且本仓库裁剪版 + HAL **没有** `HAL_ADCEx_Calibration_Start` → 由 `Src/adc.c` 的 `adc1_calibrate()` 实现 + (`USER CODE` 区 = L2,审计项 `adc1-calibration-call` / `adc1-calibration-impl` 兜底)。 + 实测校准前后同一拍 STM32 侧 6 路**同向**偏移 +10~+63 LSB、TPAFE 侧一字不变 → 此前 ADC 确实 + 带失调在跑。它**不消** 2 ms 忙等,但把"未校准"这条可疑路径从本节的候选根因里划掉。 +- **启动 fail-safe(设计如此,已按此实现)**:TIM6 未启动 / 停摆 → `adcTask` 永久阻塞于 + `osThreadFlagsWait(osWaitForever)` → `TASK_WD_ADC` 缺席 → IWDG 复位。属安全 fail-safe, + 非缺陷(确保"节拍源缺失"不被静默放过)。 + +### 10.5 下一版硬件建议:DMA + 双缓冲(方案 b,用户要求记录) + +> **2026-10-10 更新**:本节里 **STM32 侧那一半已经落地**(§11,DMA + 乒乓双缓冲 + 定时器 +> TRGO 同源触发)。原写"STM32 ADC1 已支持 **TIM6**-TRGO → ADC1 + DMA"**是错的**—— +> F4 上 ADC1 的外部触发清单里没有 TIM6/TIM7 TRGO(只接 DAC),实际用的是 **TIM3_TRGO**; +> 已按 §11 更正。下面剩下的部分是 **TPAFE5160 侧**的下一版硬件建议,仍未办。 + +瓶颈在 **TPAFE5160 接口**,须先以逻辑分析仪确认其接口类型再定型: + +- **若 TPAFE 为 SPI + DRDY**(推荐先验): + - 单 1 kHz 主时钟:TIM6 `TRGO` 同时驱动 ADC1 扫描(`ADC_ExternalTrigConv`=TIM6_TRGO)与 TPAFE `CONVST`; + - TPAFE 数据由 **SPI-RX-DMA** 搬运;`DRDY` 下降沿 → DMA **HT/TC 中断** 实现**乒乓双缓冲**; + - 任务只在"半满"时做后处理(压环 + 相量),CPU 完全脱离逐点搬运。 +- **若 TPAFE 为并行接口**:用 **FMC** 把 TPAFE 数据总线映射为异步 SRAM,DMA 从 FMC 读 → 内存; + 须确认 FSMC/FMC 地址/时序兼容 TPAFE 读时序(开销大于 SPI 方案)。 +- **统一收益**:CPU 脱离轮询/搬运 → 采样率可上探(如 4 k / 8 k 点/周波)、相量窗更密、腾出 + AboveNormal 算力给保护算法;与 §8 等周波调频天然契合(ARR 改频 + DMA 自动续传)。 +- **验收前置**:先以逻辑分析仪确认 TPAFE 接口类型与 `DRDY` 时序,**再**定型下一版硬件 DMA 方案, + 避免返工。 + +### 10.6 实施时补的一条:消费者就绪门(规划未预见,必须保留) + +规划写的是"ISR 里 `osThreadFlagsSet(adcTaskHandle, ...)`"。实施时发现**时序上不能直接这么写**: + +- TIM6 在 `app_main_init()`(`Src/main.c:140`)里就 `HAL_TIM_Base_Start_IT(&htim6)`,而 + `osKernelInitialize()` / `MX_FREERTOS_Init()` / `osKernelStart()` 在 `:144/145/148` + → **"节拍源已在跑"与"消费者已存在"之间有约 100 ms 窗口**,其间 `adcTaskHandle` 还是 NULL、 + 调度器也还没起。 +- `osThreadFlagsSet()` 对 NULL 句柄本身安全(`cmsis_os2.c` 的入参检查返回 `osErrorParameter`), + 但"句柄已赋值、调度器未启动"这段(建任务 → `osKernelStart`)仍会让 ISR 版内核 API 在调度器外跑。 + 本仓库对这类坑有实测记录:`Src/main.c` 的 **TIM7 分支注释**(2026-10-02,port.c 优先级断言 + 把系统卡死在 ISR 内)。 +- **处置**:ISR 只调 `adc_tick_irq_notify()`,其内含就绪门 `s_consumer_ready`——由 `adcTask` + 自己在进入等待前置 1(单写者 = 任务,ISR 只读)。把时序约束变成**显式代码**,而不是依赖 + FreeRTOS 的内部实现细节。 +- **实测印证**:`seq − frames = 20006 − 19999 = 7`——正是这 ≈100 ms 窗口里 TIM6 发出的 7 个脉冲 + 被丢弃(当时无消费者),之后一帧不差。 + +**教训(与 `docs/子系统设计/看门狗设计说明.md` §11.5 的三条"实施时才暴露"同类)**:把"定时轮询"换成 +"事件驱动"时,必须单独核对**节拍源启动时刻 vs 消费者创建时刻**,否则第一次上电就会踩到 +"ISR 对还不存在的消费者动手"。 + +--- + +## 11. 节拍源改 TIM3 + STM32 侧 DMA 乒乓双缓冲(v2.8,2026-10-10,**已落地并上板闭环**) + +> 触发背景:§10.4 硬化排序里的 ②(消忙等)与 ③(双缓冲)。用户明确选择最彻底的方案 +> ("我认为最彻底的就是 B2"):**DMA + 定时器 TRGO 硬件触发 + 消费上一拍已完成的半区**。 + +### 11.1 为什么节拍源从 TIM6 换成 TIM3 + +原规划(§2.1 / §10.5)假设"TIM6 TRGO 可触发 ADC1"——**在 F4 上不成立**。查 CubeMX 器件库 +(`db/mcu/IP/ADC-STM32F417_Modes.xml`)与 `stm32f4xx_hal_adc.h`,ADC1 的外部触发只有: + +``` +T1_CC1/2/3、T2_CC2/3/4、T2_TRGO、T3_CC1、T3_TRGO、T4_CC4、T5_CC1/2/3、T8_CC1、T8_TRGO、EXTI15 +``` + +TIM6/TIM7 的 TRGO 在 F4 上**只喂 DAC**(`T6_TRGO` 仅出现在 DB 里 `Expression="STM32F7"` 的块中)。 +要在**同一个硬件事件**上同时驱动 TPAFE(ISR 脉冲 CONVST)与 STM32 ADC1(TRGO), +可选的通用定时器 TRGO 只有 TIM2 / TIM3 / TIM4 / TIM5 / TIM8;TIM3 空闲且为 APB1 通用定时器 → +**改用 TIM3**(PSC 83 / ARR 999 = 1 kHz,`TIM_MasterOutputTrigger=TIM_TRGO_UPDATE`, +`TIM_MasterSlaveMode=ENABLE`)。TIM6 从 `.ioc` 中整体移除(`Mcu.IP` / `Mcu.Pin` / `VP_*` / +`TIM6_DAC_IRQn` 全部清掉),`htim6` / `MX_TIM6_Init` / `TIM6_DAC_IRQHandler` 一并消失。 + +⚠ `MSM=ENABLE` 而从模式 `SMS=0`(内部时钟)→ 软件 `CEN` 启动不受影响(已核对 RM0090 与生成代码)。 + +### 11.2 DMA 选型与三个静默陷阱 + +| 项 | 结论 | 踩坑代价 | +|---|---|---| +| 流 | **DMA2_Stream4**(ADC1 只挂 Stream0 或 Stream4;Stream1=ADC3、Stream2/3=ADC2) | CubeMX 默认抢 Stream0 → 连锁把 **SPI1_RX→Stream2、USART1_RX→Stream5**(NAND 与调试串口的 DMA 全被搬家) | +| 优先级 | `DMA2_Stream4=6`、`DMA2_Stream0=7`、`DMA2_Stream2=9` | **换流会把该流的 NVIC 优先级重置为 0** → 违反"外设中断 ≥5",0 抢占 FreeRTOS 临界区(`configMAX_SYSCALL_INTERRUPT_PRIORITY=5`) | +| `DMAContinuousRequests` | **ENABLE**(DDS 位) | 关掉 = 一轮 9 通道只发**一次** DMA 请求,静默丢 8 路,不报任何错 | +| 缓冲地址 | 必须在 **SRAM**(实测 `s_stm32_dma` @ `0x20003448`,落在 `RW_IRAM1`) | ADC1 走 **DMA2**,而 F4 的 DMA2 **不可达 CCM(0x10000000)** → 放错不报编译错、不报运行错,数据永远搬不回来 | + +模式:`DMA_CIRCULAR` + `DMA_PRIORITY_LOW` + halfword→halfword,长度 `2 × 9 = 18`。 + +### 11.3 乒乓半区怎么选才没有竞态 + +DMA 每完成 9 个半字触发一次 **半传输/传输完成** 回调,两个回调都代表"一整帧就绪", +各自 `s_dma_seq++`(`HAL_ADC_ConvHalfCpltCallback` / `HAL_ADC_ConvCpltCallback`, +均带 `if (hadc == &hadc1)` 句柄守卫)。消费侧: + +```c +half = (uint8_t)(1u - (seq & 1u)); /* seq 奇 = 刚完成前半区,偶 = 刚完成后半区 */ +src = &s_stm32_dma[half * ADC_INPUT_STM32_CH_NUM]; +``` + +**只读一个 32 位 `seq`**(原子读)→ 半区下标由它推导,不再引入第二个 ISR 写、任务读的变量 +(若另存一个 `s_dma_half` 就有"读到新 half、读到旧 seq"的窗口)。 +代价:STM32 侧数据**固定滞后 1 拍(1 ms)**——两片转换器本来就没有同刻性(N-18),无新增误差。 + +### 11.4 可观测性:四类故障分开计数 + +| 计数 | 含义 | 判读 | +|---|---|---| +| `adc1_seq` | DMA 整帧完成计数 | 应 ≈ TPAFE `seq`(实测恒定偏移 2 = TIM3 启动到 DMA 挂载之间的窗口) | +| `adc1_gap` | `seq` 一次前进 >1 → **丢帧** | 消费者超拍(单拍 > 1 ms,如心跳日志突发) | +| `adc1_err` | **连续 ≥2 拍**无新帧 → 停摆 | 节拍源/DMA 真停;单拍无新帧是**相位伪影**(见下),不计 | +| `adc1_hw` | `HAL_ADC_ErrorCallback` | ADC/DMA 硬件报错 | + +**相位伪影**(实测发现,故设 `STALL_RUN_MAX=2`):任务在 TIM3 触发瞬间被唤醒,而该拍的 DMA +完成落在 ≈67 µs 之后;若上一拍被日志/抢占拖过这 67 µs,本拍就会看到"计数没前进"(那个完成 +已被上一拍消费掉)。这种伪影**只会连续 1 拍**——DMA 若真停,第 2 拍仍不会前进。 + +### 11.5 实测(`@test` PASS,2026-10-10) + +| 指标 | DMA 前 | DMA 后 | +|---|---|---| +| `adcp`(单拍消费耗时) | **122 µs**(超时拍 2209 µs) | **8 µs** | +| `adcm`(心跳拍峰值) | 2.1 ms | 2.0 ms(仍是每 10 s 心跳日志本身) | +| `adc1_err` | — | **0** | +| `adc1_hw` | — | **0** | +| `adc1_gap` / `skip` | — | 27 / 20002 拍(0.13%,集中在开机日志突发期;稳态每 10 s 心跳 +1) | +| `adc1_seq` vs `seq` | — | 20004 vs 20006(恒定偏移 2,每拍 +1) | +| `idle` | 0 | 0 | +| 主机单测 | 79 | **101**(新增乒乓半区、HAL 回调守卫、gap/stall 分开计数三组) | +| `@build` | Code 256872 | **257660**(+788 B);ZI +4 B(36 B 乒乓缓冲 − 旧变量) | +| adcTask 静态最坏链 | 832 B / 余 59% | **832 B / 余 59%**(未变,`HAL_ADC_PollForConversion` 出链) | + +反向验证(改错实现看用例是否咬):半区奇偶取反 → 11 条 FAIL;停摆拍置 `valid=1` → 4 条 FAIL; +gap 阈值改 `>2` → 1 条 FAIL;`STALL_RUN_MAX` 改 1 → 6 条 FAIL。 + +### 11.6 遗留与注意 + +- **启动 fail-safe 语义不变但更硬**:TIM3 未启动 → ADC1 不再被触发 → `adcTask` 永久阻塞 → + `TASK_WD_ADC` 缺席 → IWDG 复位(设计如此)。 +- `adc1_gap` 的根治手段是**缩短心跳拍**(每 10 s 一次的多行日志),不是提高 DMA 能力。 +- 加大缓冲(如 4 路乒乓)前**必须**重跑 `.map` 确认地址仍在 `RW_IRAM1`。 +- 每次改 DMA 流之后必须回 NVIC 页核对优先级(§11.2 第三行)。 diff --git a/docs/子系统设计/IEC104上行设计说明.md b/docs/子系统设计/IEC104上行设计说明.md new file mode 100644 index 00000000..bc0bf441 --- /dev/null +++ b/docs/子系统设计/IEC104上行设计说明.md @@ -0,0 +1,177 @@ +# IEC 60870-5-104 上行设计说明(B4) + +> **定位**:DTU 上行规约 IEC 60870-5-104(DL/T 634.5104-2009)的设计基线—— +> 范围、点表、链路流程、板端接入、预算与测试。规格来源 +> `docs/算法/继电保护功能复现规划.md` §1.5(B0 产品定义,验收 = 省电科院点表); +> 代码落点 `App/protocol/iec104/`;登记项 `docs/任务规划/W1-采集保护收尾.md`(W1 / B4)。 +> **角色**:本 DTU = **被控站(受控端)**,104 **TCP 服务器,端口 2404**;主站 +> (控制端,点表参考 `192.168.1.199`)主动连入。 + +## 1. 范围(V0.1 子集) + +| 面 | 本期做 | 本期不做(登记 §8) | +|---|---|---| +| 链路 | APCI U/S/I、STARTDT/STOPDT/TESTFR、k/w 窗、t1/t2/t3 | 多主站并发(单连接语义,见 §5) | +| 遥测 YC | 短浮点 M_ME_NC_1(系数 1):背景/周期/死区上送 | 归一化/标度、谐波轨(L2-8) | +| 遥信 YX | 单点 M_SP + 双点 M_DP:背景 + 突发(COT=3) | SOE 带时标 CP56(M_SP_TB,随 L4-4 SOE 落地) | +| 遥控 YK | 双点 C_DC 两段式(选择 82H/执行 02H) | 带时标、升降/步位 | +| 定值 | C_RD 读 + C_SE_NC 两段写(地址 = prot_set `comm_addr`) | 定值区切换/下装文件 | +| 其他 | C_CS 对时(→ `sys_clock`)、C_TS、C_RP | 固有参数 8001H~800AH(字符串类)、C_CI 电度、文件传输(录波) | + +## 2. 分层与模块(A-4 约束 1:协议解析与硬件 I/O 分离) + +| 模块 | 文件 | 职责 | I/O | +|---|---|---|---| +| ASDU 编解码 | `App/protocol/iec104/iec104_asdu.c/h` | 类型/COT 常量、ASDU 头、IOA 3B 小端、短浮点、CP56Time2a↔epoch | 零 | +| 点表/数据源 | `App/protocol/iec104/iec104_pts.c/h` | YC/YX/YK/定值表、COV(变位+死区)、**数据源 vtable**(板端绑 phasor/prot_*,单测绑假源) | 零(源注入) | +| 链路状态机 | `App/protocol/iec104/iec104_link.c/h` | TCP 流重组、U/S/I、k/w/t1/t2/t3、总召/突发/遥控/定值/对时编排 | 零(收=on_rx 喂流,发=回调,时基注入) | +| 板端胶水 | `App/task/iec104_serv.c/h`(随实现定名) | 2404 监听、数据通道收发、断链重听、netTask 驱动 | 全部(net_* API) | + +> 三个纯逻辑模块 + 真实数据源(`App/algo/` 全纯逻辑)可整体在主机侧编译 → +> L0.5 单测覆盖**全协议栈**(`test/host/iec104/`),与 `test/host/prot_elm/` 同模式。 + +## 3. 点表(V0.1) + +### 3.1 遥测 YC(IOA 0x4011+,短浮点,系数 1) + +| IOA | 点 | 来源 | V0.1 状态 | +|---|---|---|---| +| 0x4011 | 1Ua | phasor `C02-UA-F1` fAmSQ | ✅ 有效 | +| 0x4012/0x4013 | 1Ub/1Uc | — | ❌ 发 IV(STM32 侧削顶,`docs/算法/继电保护功能复现规划.md` §1.6) | +| 0x4014 | 1U0 | phasor `C03-UN-F1` | ✅ 有效(外部零序专用通道) | +| 0x4015 | 1Ia | phasor `C01-IA-F1` | ✅ 有效 | +| 0x4016~0x4018 | 1Ib/1Ic/1I0 | — | ❌ 发 IV(同上) | +| 0x401D | 1f 频率 | 软件测频 | ❌ 发 IV(L1-7 未实现) | + +- 现读数为**原始码值**(L1-5 标定未实现,系数=1 口径与之一致);死区本期内置 + 10.0 码值/点,标定落地后复核。 +- 相量未就绪(未满一周波)→ QDS 置 NT(0x20),与 IV(无效通道)语义区分。 + +### 3.2 遥信 YX(IOA 1~33) + +| 段 | 点 | 来源 | V0.1 状态 | +|---|---|---|---| +| 1~4 | 开关位置(双点) | 无出口/位置硬件 | ❌ IV | +| 5~14 | 预留 | — | ❌ IV | +| 15 | 故障总 | OC1∨OC2∨OC3∨3U0(`prot_elm`) | ✅ | +| 16~18 | 过流 Ⅰ/Ⅱ/Ⅲ 段 | `PE_OC1/2/3` asserted | ✅ | +| 19~20 | 零序过流 Ⅰ/Ⅱ | L3-2 未实现 | ❌ IV | +| 21~22 | 接地故障/重载 | 未实现 | ❌ IV | +| 23 | 过载 | `PE_OL` asserted | ✅ | +| 24~30 | 外部遥信(RS-485) | B2 暂缓(`docs/任务规划/暂缓-RS485.md`) | ❌ IV | +| 31 | 保护闭锁 | `prot_set_locked()` | ✅ | +| 32~33 | 预留 | — | ❌ IV | + +> **点存在性优先**:无效点照常入总召/周期表并置 IV——点表是验收口径,缺行比 +> 置 IV 更糟(主站侧无法区分"没做"与"接线错")。 +> +> **保护类 YX(15/16~18/23/31)自 B5(2026-10-12)起为活数据**:protectTask +> 20 ms 判据节拍驱动 `prot_elm` 状态 → pts COV 变位上送(≤120 ms 端到端)。 + +### 3.3 遥控 YK(IOA 0x6000+,双点两段式) + +| IOA | 点 | V0.1 行为 | +|---|---|---| +| 0x6000~0x6003 | YK1~4 开关 | **否定确认**(无出口硬件,`docs/算法/继电保护功能复现规划.md` R7) | +| 0x600A | YK11 信号复归 | ✅ 执行段 → `prot_elm_request_reset()`(B5 起跨上下文安全:请求由 protectTask 下一拍 eval 头部消费,不当拍直接复位) | +| 0x600B | YK12 遥控软压板 | ✅ 两段式转真(B7):选择段校验 DPC(ON/OFF,非法即 PN);执行段 → `prot_set_set_sealed(PS_GLOBAL_EN)` 原子写+封存;protectTask 下一拍读到退出态即复位元件并停判据(`App/task/protect_task.c`) | +| 0x600C | YK13 电池活化 | 否定确认(无对应硬件) | +| 0x600D | YK14 解除闭锁 | 否定确认(闭锁源于定值校验和,只能重写定值解除) | + +### 3.4 定值(IOA = 通讯地址) + +- 地址 = `App/algo/prot_set.h` 元数据 `comm_addr`(0x8820 基址 + 告警段 0x8027~0x802A, + 逐项对照参考项目 `Const_CommSetPara.c`,见 `docs/算法/继电保护功能复现规划.md` §5.4)—— + **点表不另立地址表**,prot_set 即单一事实源。`comm_addr = 0` 的本地项(投退/返回系数/ + 全局软压板)不可经 C_RD/C_SE 寻址(B7 起显式排除,防 IOA=0 误命中首个本地项)。 +- 写两段式:选择 = 范围校验;执行 = `prot_set_set_sealed`(**原子写+封存**:影子区 + 写好后单字交换活跃区,protectTask 的 L5-1 巡检任何时刻都看到自洽快照, + B5 登记的 µs 级竞态窗口就此收口)。 +- 持久化(B7):执行段置 prot_set 脏标志 → iec104Task 每轮 poll `dirty_peek` 投递 + `SYS_EVT_PROT_SET_SAVE` → **defaultTask(配置引擎属主)** 经 `App/cfg/prot_set_store.c` + 原子写 `/prot_set.bin`(blob = magic+版本+计数+值区+sum32 正/反码对); + 开机在 STORAGE_READY 屏障前装载,损坏/越界/版本换代 = 整表拒绝保持出厂默认 + (all-or-nothing)+ 自检留痕 `prot set corrupt`;写盘失败脏标志保留 = 下轮重试。 + +## 4. 链路与流程 + +- 参数:k=12 / w=8 / t1=15s / t2=10s / t3=20s(标准默认);序号 15bit 回绕, + 超时判定一律差值法(tick 回绕安全,同 `App/algo/` 惯例)。 +- 状态机:IDLE→(TCP 建立)OPEN→(STARTDT act/con)RUN⇄STOPPED;t1 超时或坏帧 + → 实例回 IDLE,**由胶水层负责断 socket**(纯逻辑层不感知 I/O)。 +- 总召(C_IC 组 1):actcon → 分片背景扫描(YX 双点组 SQ=1 → YX 单点组 SQ=1 → + YC 组 SQ=1 → 频率单点)→ actterm;首召/ RBT 位 → COT=4(初始化),否则 COT=2。 + 单次 poll 发送预算 4 个 ASDU(防大扫描饿死宿主任务)。 +- 突发:COV = 遥信变位 + 遥测死区(`iec104_pts_cov_check`),COT=3 逐点 SQ=0。 +- 周期:RUN 态 60 s 全 YC 上送 COT=1。 +- 遥控/定值写:两段式,失配/越界 → COT|PN 否定确认;未知 IOA → COT=14+EVI、 + 错公共地址 → COT=13+EVI、未知类型 → COT=12+EVI。 +- 对时 C_CS:CP56 → epoch ms → 胶水层 `sys_clock` 设置(IV 位置起即 PN)。 + +## 5. 板端接入(iec104Task 胶水,2026-10-12 实现回填) + +- **承载任务 = 专用 iec104Task(640 words / Normal)**,非 netTask:netTask 是 + socket RPC 执行者,`net_recv/net_send` 在 netTask 内调用 = 自己等自己回执(死锁)。 + 手写于 `Src/freertos.c` USER CODE 区(timeSyncTask/adcTask 同款先例),文件 + `App/task/iec104_serv.c`(纯编排,协议语义全在 `App/protocol/iec104/`)。 +- **看门狗**:`TASK_WD_IEC104 = 7` 纳入 `wd_required_mask()` 两分支(产品 0x1F7 / + HW 测试层 0x1F5,含 B5 protect 位 bit8);主循环 100ms 节拍喂位,屏障等待按 + 100ms 分片喂位(timeSyncTask 2026-10-02 台架先例)。 +- **就绪屏障**:等 `SYS_EVT_NET_READY`(`osFlagsNoClear` + 分片)后才建监听。 +- **单连接语义**(`AGENTS.md` 模块约定,抄 `ui_net_sink.c` 模板):2404 监听与 + 数据通道复用同一 socket(accept 返回即 listen fd);EOF 时 net 层 auto_relisten, + 应用层**不得**关 fd(`client == listen` 分支);仅模拟器多连接后端(fd 不同)显式关。 +- **门控 + 兜底**:`net_can_accept`/`net_can_read`(纯 RAM 读)省无谓 RPC 往返; + 每 32 轮无条件 recv 一次防 RECV_OK 中断丢失(ui_net_sink I-1/I-2 先例)。 +- **网络代次自愈**:`net_get_generation()` 变化 → 关旧 fd 重建监听(netTask + NET_RECONFIG 重建路径的既有约定)。 +- **主动拆链**(与 EOF 路径相对):协议故障(坏帧/N(R) 倒退/t1 超时)或发送连续 + 3 次失败(E-04 短写先例)→ 本端 `net_close` + 重建监听,促主站重连——违例时 + 主站不会自行断开,必须本端断(time_sync 会话收尾先例,非芯片 auto_relisten 风暴路径)。 +- **抢占策略修正(V0.1 如实登记)**:建档时定"后连优先",实现后确认单连接 + socket 在连接态无法 accept(监听即数据通道),**V0.1 实为首到优先**——旧会话 + 经 EOF/t1/拆链结束后新主站才能接入;后连优先需 net 层支持,见 §8 第 7 条。 +- **k/w 与 RPC 时延**:`net_send` 经 netTask 串行 RPC(最坏往返无界 ≈4 s); + t1/t2/t3 全部用注入 tick 判定,单次 RPC 停顿不会误触 t1(15 s),连续停顿由 + "发送 3 连败拆链"兜底。COV 突发延迟 ≤100ms(服务周期),远小于 SOE 5000ms 线。 +- **对时**:C_CS → `sys_clock_set_unix()`(含 SD2506 RTC 写回);epoch 秒 >2^32 + (2106-02-07 后)显式拒绝回 PN。公共地址 V0.1 固定 1。 + +## 6. RAM / 栈预算(2026-10-12 实测) + +| 项 | 实测 | 依据 | +|---|---|---| +| 静态(新增) | link 实例 ~560 B(含 512 B 流重组缓冲)+ pts 快照 ~120 B + 胶水 rx 128 B | `iec104_link_t` 字段和 | +| iec104Task 栈 | 640 words(2560 B):静态最坏链 1456 B(拆链→重建监听→net_bind→SPI 故障→日志尾),余 1104 B / 43% **PASS** | `stack_report.py` 2026-10-12;512 words 时余 592 B < ISR 底线 → 抬栈 | +| CCM 堆 | 任务栈 2.5 KB + TCB,min_ever 6,640 → ≈6,100 B(> 3,072 告警线) | adcTask 抬栈同口径台账 | +| 总量门禁 | RW_IRAM1 73.1% / RW_CCM 75.3%(<92%) | `ram_baseline.py --update` 2026-10-12 | + +## 7. 测试方案 + +| 层 | 内容 | +|---|---| +| L0.5 | `test/host/iec104/`:APCI 重组/坏帧、STARTDT 握手、总召全流程(含 RBT/分片/预算)、COV、k/w 窗、t1/t2/t3(假时基)、遥控两段式与 PN、YK12 软压板两段转真 + 置脏(B7)、定值两段写 + set_sealed 后 selfcheck、C_RD(含本地项不可寻址)、C_CS(含闰年)、错地址/错类型 EVI;`test/host/prot_set/`:blob 往返 + 损坏整表拒绝(all-or-nothing)| +| L0 | `iec104-unit-test` 钩子 + cppcheck/coding-standard | +| 台架 | `@test` 锚点(104 服务启动/监听行);**PC 主站客户端 `test/host/iec104_master.py`(纯标准库,B6 2026-10-12 就绪)**:STARTDT/总召四组+actterm/C_SE 定值两段写→YX15·16 变位(COT=3)/恢复复归/YK11 两段正答/C_RD 读回,台架往返 **18/18 PASS**(证据 `../测试说明/boot_log_data/20261012_B6_iec104_roundtrip_log.txt`);**B7 持久化模式 `--b7-pre/--b7-post/--b7-mark/--b7-check`**(中间 ST-LINK 复位):YK12 两段投退→保护退出/联动、**定值与压板态跨复位保持**(含非默认 7.5 往返),**21/21 + 13/13 + 4/4+4/4 PASS**(证据 `../测试说明/boot_log_data/20261012_B7_iec104_persist_log.txt`);lib60870 级第三方全流程归 B10 | + +## 8. V0.1 限制与待办(如实登记) + +1. 固有参数 8001H~800AH 未实现(字符串类无法短浮点上送)→ 未知 IOA 应答;随送检需求再设计。 +2. SOE 带时标(M_SP_TB_1)随 L4-4 SOE 记录落地;现突发只发 COT=3。 +3. YK1~4/13/14 否定确认——出口硬件确认后(R7)转真执行(YK12 已于 B7 转真)。 +4. 遥测死区 10.0 码值为拟定值,L1-5 标定后按工程量死区(点表 8020H 段)复核。 +5. 多主站/双链路冗余不做(单连接语义)。 +6. 主站侧验证工具 = `test/host/iec104_master.py`(B6 2026-10-12 就绪,台架往返 18/18,覆盖总召/变位/遥控/定值);**第三方送检环境仍未就绪**——lib60870 级第三方全流程归 B10。 +7. **后连优先未实现**(V0.1 = 首到优先):单连接 socket 连接态无法 accept, + 旧会话结束前新主站接入被拒;后连优先需 net 层(CH395F 单连接模型)支持, + 真需要冗余切换时随 net 扩展立项。 +8. 公共地址固定 1、监听端口固定 2404:可配置化(config.ini)随二期。 + +## 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-12 | 建档(B4):范围/分层/点表/链路流程/接入决策/预算/测试方案;§5 接线细节随实现回填 | +| 2026-10-12 | B4 落地回填:承载改**专用 iec104Task**(netTask 承载会 RPC 自死锁);§5 全量回填(门控/代次/拆链/对时/看门狗位 7);抢占策略修正为**首到优先**(§8 第 7 条);§6 实测(640 words,链 1456 B,余 43% PASS) | +| 2026-10-12 | **B7 定值持久化 + 软压板**:YK12 转真(§3.3,两段式 → `PS_GLOBAL_EN`);定值执行段改 `prot_set_set_sealed` 原子写+封存(§3.4,收口 B5 竞态窗口);本地项 `comm_addr=0` 不可寻址;持久化 = 脏标志 → `SYS_EVT_PROT_SET_SAVE` → defaultTask 原子写 `/prot_set.bin`,开机屏障前整表原子装载(损坏拒绝保默认 + 自检留痕);单测 +7(iec104 94 断言)+ prot_set blob 往返/损坏 33 断言 | +| 2026-10-12 | **B7 台架腿闭环**(§7 台架行回填):主站工具加 `--b7-pre/--b7-post/--b7-mark/--b7-check` 四模式(中间 ST-LINK 复位);pre 21/21(YK12 OFF→低定值不动作 / ON→YX15·16 即动作)、post 13/13(**压板退出态跨复位保持** + 重启后联动在位)、mark/check 4/4+4/4(**非默认定值 7.5 跨复位保持**);开机日志 `[CFG] prot set loaded (/prot_set.bin)`;证据 `../测试说明/boot_log_data/20261012_B7_*` | diff --git a/docs/子系统设计/控制台单写者日志设计说明.md b/docs/子系统设计/控制台单写者日志设计说明.md index 5ac5fdc8..da95515b 100644 --- a/docs/子系统设计/控制台单写者日志设计说明.md +++ b/docs/子系统设计/控制台单写者日志设计说明.md @@ -43,7 +43,7 @@ HAL 并发语义(本仓库实现实测,`stm32f4xx_hal_uart.c:1135`):轮 | # | 约束 | 出处 | |---|------|------| | K1 | 内核启动前禁止创建/使用任何 FreeRTOS/CMSIS-RTOS 对象 | AGENTS.md 平台约束(2026-09-20 启动失败实证根因)→ 本模块内核前纯 BSS/寄存器机制 | -| K2 | 禁止 `malloc/calloc/realloc`(C 堆仅 512B) | AGENTS.md P0-1 → 全部状态静态 BSS | +| K2 | 禁止 `malloc/calloc/realloc`(C 堆仅 512B) | AGENTS.md 平台约束(禁 C 库堆分配条)→ 全部状态静态 BSS | | K3 | HAL 生成文件(`Src/`、`Inc/`)不含应用代码 | 2026-09-25 迁移动机 → 实现位于 `App/log/` | | K4 | 生产者阻塞时间有界 | 最多 `DBG_CON_WAIT_MS`(200ms),日志不得阻塞业务 | | K5 | RAM 预算紧张,新增静态内存必须核算 | §8 | @@ -114,7 +114,7 @@ flowchart LR | 写者未注册(内核启动 → attach) | **约定只有 `defaultTask` 记日志** | 遵守约定 → 无并发;**其他任务此时记日志 → 逐字节交错**(裸发无互斥) | | 写者已注册(稳态) | 任务上下文唯一调用者 = 写者 | 任务上下文零竞争;残余窗口 = ISR 直发撞写者发送中(丢失、不挂死,§7) | -**纪律(2026-10-10 固化)**:周期 / 常驻任务**不得**在写者注册前记日志——用 `dbg_console_writer_ready()`(§5)门控自身日志(`adcTask` 的首帧与 10 s 心跳即按此门控)。内核前代码仍可直接裸发(单线程,无并发)。 +**纪律(2026-10-10 固化)**:周期 / 常驻任务**不得**在写者注册前记日志——用 `dbg_console_writer_ready()`(§5)门控自身日志(`adcTask` 的首帧与 10 s 心跳、`protectTask` 的启动锚点行即按此门控——后者 2026-10-12 B6 台架实证:B5 接线时启动行未门控,注册前裸发被交错损坏致 `@test` 锚点 MISS,见 §11 变更记录)。内核前代码仍可直接裸发(单线程,无并发)。 ## 4. 详细设计 @@ -307,6 +307,7 @@ sequenceDiagram | 2026-10-02 | **ORE 僵尸通道喂狗节奏保护**(台架 gen-288):round-3 的"单次 drain 预算 250ms"只限单次调用——ORE 锁存在外设软复位后**毫秒级重锁**(PA10 悬空 + RX 使能),背靠背 drain 每轮再耗 100ms 块超时 → 主循环周期拉到 450~500ms → 100ms 节流的喂狗抽样间隙超过 IWDG 最坏容限 1.7s → 停喂 → 误复位(且心跳位全员健康、无 stall 日志,纯**节奏性饥饿**,非死等)。修复:发送故障后立即结束本轮 drain 并进入 300ms 发送冷却(冷却期内 drain 直接让位主循环),保证喂狗节奏 ≤ ~300ms | | 2026-10-02 | **round 8b 加固**:发送超时判据改用 DWT `CYCCNT`(时间源无关)。原 `HAL_UART_Transmit` 的 `HAL_GetTick() - Tickstart` 依赖 TIM7 的 `uwTick`,TIM7 一旦停摆超时永不成立 → 写者死等 TXE → 唯一喂狗者停摆 → IWDG 复位。裸发送与缓冲满等待两处均换成 DWT 周期计数硬上界(DWT 使能失败时退回旧 HAL 行为);另加 `huart1.Instance == NULL` 短路,防早期/断言路径解引用 NULL | | 2026-10-10 | **新增 `dbg_console_writer_ready()` + 未注册期纪律固化**:裸发为寄存器级轮询(无 `gState`)→ 未注册期多任务并发**逐字节交错**(台架 `adcTask` 首帧日志与 `[NAND_FTL]` 行交错实证)。新增 `dbg_console_writer_ready()` 供周期任务门控;`adcTask` 首帧/心跳日志据此门控(§3.4 / §4.4) | +| 2026-10-12 | **纪律盲区台架实证(B6)**:`protectTask`(B5 接线,台架腿当时未跑)启动锚点行在写者注册前裸发 → 与 defaultTask 启动日志**逐字节交错**(`st[art: cycle=220ms elm=70`,两次开机同形复现 = 启动调度确定性的竞态,非 USB 噪声)→ `@test` 锚点 MISS。修复 = 启动行改 `dbg_console_writer_ready()` 首拍补打(adcTask 首帧同款),`@build` 0/0 + `@test` PASS 复验(锚点完好 +577 ms)。§3.4 纪律示例更新 | ## 12. ORE 僵尸通道(2026-10-02 台架实证) @@ -323,7 +324,7 @@ sequenceDiagram **未做(根治)**:让 USART1 为 **TX-only**(.ioc 中不再使能 RX / PA10 不作 USART1_RX), 从机制上消除 ORE。属 `.ioc` 结构性改动 + CubeMX 重生成,需单开一轮并复跑台架闭环 -→ 已登记为 `待处理内容.md` **S-3**。 +→ 已登记为 `docs/任务规划/登记簿.md` **S-3**。 ## 13. 开放问题 @@ -332,7 +333,7 @@ sequenceDiagram | O1 | 未注册期裸发无互斥 | 启动窗口(内核启动 → attach)`dbg_console_write_raw` 无互斥 → 多任务并发会**逐字节交错**(非旧 `HAL_BUSY` 丢整行)。2026-10-10 已固化"周期任务用 `dbg_console_writer_ready()` 门控"纪律;若未来开机日志密度上升,可升级为"内核前静态待发行 + attach 后统一补发"或给裸发加轻量互斥 | | O2 | `HAL_UART_Transmit` 超时未计数 | 超时块(UART 物理失效)丢弃但不入 `s_con_drop`;v2 可加独立超时计数 | | O3 | `printf` 逐字节开销 | 遗留 `printf` 路径 = 逐字节临界区 + 逐字节事件置位;遗留调用点全部切 `DBG_*` 宏后开销自然消失(不改变机制) | -| O4 | **ORE 僵尸未根治** | §12:现仅有缓解(250ms 预算 / 300ms 冷却 / DWT 超时),机制根源是 USART1 使能了 RX。根治需 `.ioc` 改 TX-only + CubeMX 重生成 → 已登记 `待处理内容.md` **S-3** | +| O4 | **ORE 僵尸未根治** | §12:现仅有缓解(250ms 预算 / 300ms 冷却 / DWT 超时),机制根源是 USART1 使能了 RX。根治需 `.ioc` 改 TX-only + CubeMX 重生成 → 已登记 `docs/任务规划/登记簿.md` **S-3** | ## 14. 相关文档 diff --git a/docs/子系统设计/看门狗设计说明.md b/docs/子系统设计/看门狗设计说明.md index 32af3078..e44f28db 100644 --- a/docs/子系统设计/看门狗设计说明.md +++ b/docs/子系统设计/看门狗设计说明.md @@ -6,13 +6,13 @@ > **日期**:2026-09-29 创建;**2026-10-10 按当前代码重写正文**(旧正文含大量立项前的 > 描述,已清理;实施史与逐次决策见 §10 与文末变更记录,未删)。 > **相关文档**: -> [`../待处理内容.md`](../待处理内容.md)(未办项 N-17 / S-3 / N-4)、 +> [`../任务规划/README.md`](../任务规划/README.md)(登记簿未办项 N-17 / S-3 / N-4,见 `../任务规划/登记簿.md`)、 > [`运行与自检记录设计说明.md`](运行与自检记录设计说明.md)(复位记录与 `selftest.log`)、 > [`../测试说明/看门狗单元测试说明.md`](../测试说明/看门狗单元测试说明.md)(L0.5 判定状态机单测)、 > [`../测试说明/实机测试实施指南.md`](../测试说明/实机测试实施指南.md)(故障注入台架流程)、 > [`../架构设计/项目启动顺序管理.md`](../架构设计/项目启动顺序管理.md)(启动时序与就绪屏障)、 > [`../测试说明/ST-LINK_CLI_复位使用说明.md`](../测试说明/ST-LINK_CLI_复位使用说明.md)(复位语义)、 -> [`../问题记录/项目审查与优化建议报告.md`](../问题记录/项目审查与优化建议报告.md)(N-03 原始登记,已销项) +> [`../问题记录/项目审查与优化建议报告.md`](../问题记录/项目审查与优化建议报告.md)(N-03 的立项出处;该条已修复并按「修复即销项」从报告删除,**完整记录即本文**) --- @@ -24,7 +24,7 @@ | 超时 | 标称 **2498 ms**,最坏(LSI 47 kHz)**≈1701 ms** | `WD_IWDG_PRESCALER=4` / `WD_IWDG_RELOAD=1249`(`task_watchdog.c`) | | 抽样周期 | **100 ms**(`WD_POLL_PERIOD_MS`) | 同上 | | 缺失容忍 | **连续 8 轮 = 800 ms** 后停喂(`WD_MISS_TOLERANCE`) | 同上 | -| 必需掩码 | 产品构建 **`0x77`**(default/net/ftp/ui/timesync/adc);`ENABLE_HW_LAYER_TESTS` 下 **`0x75`**(剔 net) | `wd_required_mask()` | +| 必需掩码 | 产品构建 **`0x1F7`**(default/net/ftp/ui/timesync/adc/iec104/protect);`ENABLE_HW_LAYER_TESTS` 下 **`0x1F5`**(剔 net) | `wd_required_mask()` | | 喂狗者 | **仅 defaultTask**(`task_wd_poll`,主循环每轮调用、内部节流) | `App/task/default_task.c` | | 启动时刻 | **defaultTask 存储 bring-up 之后**(`task_wd_start()`);`MX_IWDG_Init()` 只填参数不启动 | `default_task.c:175`、`Src/iwdg.c` 的 `USER CODE BEGIN 0` | | 复位归因 | 停喂 → IWDG 自然溢出 → `RCC_CSR.IWDGRSTF`(不用 `NVIC_SystemReset`) | `task_wd_poll()` | @@ -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` + 驱动已挂载),复位原因每次开机都在上报。 --- @@ -456,6 +456,7 @@ snapshot == required → miss_streak=0、证据门闩复位、喂狗 | R4 | **NAND 真实卡死时 FTL 直接喂狗** | 设备存活但存储功能冻结(与 ORE 自愈同类边界)。若要求"NAND 死也必须复位",需给 `nand_ftl_wd_feed()` 加次数/时限上限 | | R5 | **栈溢出与断言在复位原因上不可区分**(都是 `SFTRSTF`) | 可接受;串口分别打 `[ERR] STACK OVERFLOW` / `[ERR] ASSERT FAILED`,`selftest.log` 侧另有记录 | | R6 | **`RCC_CSR` 读后即清** | 已在 `main.c` 统一处理;漏清会让下次软复位读回上次标志 | +| R7 | **UI 推流的网络往返卡死不占心跳位**(产品掩码仍 `0x77`) | 刻意:往返卡死的根因在 netTask,netTask 已在掩码内、由它暴露;为"远端显示卡了"新增一位只会把观感问题升级成整机复位。UI 心跳已由 P1-4a 自产、不受推流影响;推流耗时看 `[PROF]` 的 `uis`/`uisn`。成因与判读见本文 **§11.3 第 2 条**;门控方案与残留暴露见 `../UI设计/RemoDispBus协议与接收设计说明.md` §3 | ### 9.2 未办项(登记簿同步) @@ -517,6 +518,108 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建 > **工作流**:每次 CubeMX 重生成后**立刻**跑一次该脚本——它直接列出"被覆盖了哪几处 + 修复提示",比事后靠故障现象反推快得多。 +### 10.4 ARMCC `__asm` 的三条语法约束(2026-09-29 踩坑) + +> **出处**:自 `AGENTS.md`「ARMCC 内联汇编语法」条目迁入(AGENTS.md 瘦身,不再当事实存放地)。4 个 Fault Handler 是手工 `__asm`(§10.2 的 L2 条);ARMCC 的内联汇编只有一种合法形态,以下三种写法全部实测踩坑: + +| # | 尝试写法 | ARMCC 实际报错 | 根因 | +|---|---|---|---| +| ① | `__asm volatile ("MRS %0, lr" : "=r" (x))` | `#18: expected a ")"` | 这是 **GCC 内联汇编语法**,ARMCC 不支持 | +| ② | `__asm { MRS x, lr }`(块内联) | `#20: identifier "lr" is undefined` | 块内联把寄存器名当 **C 标识符**解析 | +| ③ | `__asm` 块内 `ADR r1, xxx_tag` 取 C 侧字符串/表格 | `A1516E: Bad symbol 'xxx_tag'` | `__asm` 块里**取不到 C 的数据符号**;去掉 `static` 也无效——汇编器只认自己作用域内的符号 | + +**结论 / 可用写法**:整个函数写成 `__asm void f(void) { ... }`;要传"字符串/表格"这类 C 侧数据,就让 `__asm` 只取**寄存器参数**然后 `B` 到一个普通 C 函数、由 C 侧补上——所以 4 个 Fault 的故障名必须由 C 侧 per-fault 入口 `fault_entry_{hard,mem,bus,usage}` 带上,`__asm` 里不取 C 变量。 + +**覆盖 CubeMX 生成的同名函数**(对 §10.2 那条的补充):用 `#define f f_Gen`(**写在生成实体之前**)把生成实体改名,链接器会把未被引用的 `f_Gen` 自动移除(`.map` 可见 `Removing ... (N bytes)`)——**零代码开销且不删生成区任何一行**。 + +--- + +## 11. 任务级饥饿的诊断方法与实测基线(Task #32 沉淀,2026-10-10) + +> **来源**:`[ERR][WD] task stall: mask=0x00000067 req=0x00000077`(缺 bit4 = `TASK_WD_UI`) +> 的完整归因与处置。故障本身已结案——**累计 38 次上板启动 0 复现**(原 L2 基线 3/10), +> 原始分析文档按登记簿"完成即销项"规则删除。本节只留**可复用的判读口径、诊断方法、 +> 实测基线与教训**;ADC 侧的处置细节在 [`ADC采样节拍设计说明.md`](ADC采样节拍设计说明.md) §10/§11, +> UI 推流门控在 `../UI设计/RemoDispBus协议与接收设计说明.md` §3,就绪门控 API 在 +> `../网络/BSD_Socket_API_使用指南.md` §10.1。 + +### 11.1 现场日志的三条判读口径(都被误读过) + +| 现象 | 正确读法 | +|---|---| +| 启动期出现 `MISS start snap=0x67 req=0x77` | **良性 episode**:`[WD] armed` 在 bring-up 完成后武装,而 `required_mask` 静态含 UI 位 → UI 首跳之前必现一次,随后自行恢复。**不是故障判据**,只作"UI 尚未起跳"的旁证 | +| `miss=11` / `miss=12` | `miss=` 是**累计**缺席次数(`s_miss_total`),**不是连续次数**;连续次数 `s_miss_streak` 饱和在 `WD_MISS_TOLERANCE=8`。缺口要用 `t=`(毫秒时基)差算,本例恰为 800 ms | +| `reset cause: software (NVIC_SystemReset)` | **不能**用来区分"IWDG 溢出"与"台架拉 NRST / 烧录工具软件复位"——本台架**健康**启动同样报这一串。停摆的权威证据是 `[ERR][WD] task stall` 行本身 | + +### 11.2 诊断四步:先量化,再归因 + +1. **看掩码缺哪几位。** 只缺**一位** = 定点打击(该任务独有的依赖或优先级处境); + **多位同时缺** = 全局饥饿 / CPU 饱和。本例 `0x67` 只缺 UI,其余五个 Normal 任务心跳 + 全正常 → **当场否掉"计算量把 CPU 吃满"这个直觉**(用户假设方向对、结论不成立)。 +2. **把嫌疑任务的单拍成本逐项算出来**(不要估)。当时算式:ADC1 一轮 9 通道 ≈67 µs、 + TPAFE 快照 + 9 路取数 ≈10~20 µs、压环 + 5 实例傅氏 ≈10~15 µs → 单拍 0.10~0.13 ms + = **占空比 10%~13%**,距饱和很远。 +3. **用计数速率反证"有没有空转"。** `ring` 在两次心跳间严格 +10000/10 s = **恰好 1000 次/秒**; + 若该任务失去阻塞(空转),按 0.11 ms 单拍应为 ≈9000 次/秒 → 反证它每拍都睡满。 +4. **用 `[PROF]` 指认最大非让出占用**(§11.4)。实测最大是 `defaultTask` 控制台排空 + ≈46 ms,**没有任何任务接近 800 ms** → 800 ms 属**异常长尾**,只能靠复现现场的快照抓, + 不能靠健康剖面指认;⚠ 别把"健康剖面里没有"当成"不存在"。 + +### 11.3 三条结构性脆弱点(当时全部从代码证实) + +| # | 脆弱点 | 现状 | +|---|---|---| +| 1 | **uiTask 是 `required_mask` 里唯一的 `osPriorityBelowNormal`**(其余五个全 `Normal`)。FreeRTOS 数值越大优先级越高 → 任一 Normal 任务持续就绪,uiTask 就**完全拿不到 CPU**;`configUSE_TIME_SLICING=1` 只保护**同优先级**公平 | **至今未变**(刻意不提优先级,理由见 §11.6)。风险由"心跳自产 + 往返门控 + 采集零忙等"三面压住 | +| 2 | **UI 心跳曾依赖 netTask 往返**:`ui_net_sink_poll()` → `net_accept()` → `osMessageQueuePut(osWaitForever)` + `xTaskNotifyWait(portMAX_DELAY)`,**之后**才 `task_wd_beat(TASK_WD_UI)` → netTask 慢一拍、UI 心跳晚一拍。这正是"**只有 UI 位缺失**"的根因(别的任务没这条依赖) | ✅ 已断两处:心跳提到 poll **之前**(`App/task/ui_task.c`);无客户端时门控让往返 = **0 次**(`uis` 实测 1 µs) | +| 3 | **自定截止期一旦错过就不可恢复**:`next += 1; osDelayUntil(next)` 且**丢弃返回值** → 单拍 >1 ms 后 `next` 相对当前时刻永久落后 → 该任务从此不再阻塞、以 Normal 优先级空转(**连续**慢拍才锁死,单次慢拍会自己追平)。返回值本是现成的重同步钩子 | ✅ 已消:adcTask 改**事件驱动唤醒**(`osThreadFlagsWait`),"落后"这个概念不存在了(`ADC采样节拍设计说明.md` §10) | + +⚠ 顺带揪出的两个**独立正确性**缺陷(与停摆无关,但同源发现,别只当性能问题): + +- **无新帧仍压环**:`r == 0`(TPAFE 无新帧)时也压帧 → 上一拍的重复值进 20 点 DFT 窗, + 等效采样间隔被拉长。当时 `idle` 占 3.2% → 窗长从 20.0 ms 漂到 ≈20.6 ms ≈ **3% 频率失配** + → 幅值/相位**系统性误差**(不是随机噪声)。现仅 `r == 1` 压帧,上板印证 `ring == frames`。 +- **失败拍静默沿用上一拍值**:失败支路不清 `s_stm32_valid` → 照样压环、`skip` 抓不到。 + 现失败即清标志走整帧跳过(`ADC采样节拍设计说明.md` §10.4;单测 TC-AI-070..078 钉死)。 + +### 11.4 `[PROF]` / `[PROF2]` 字段语义与当前基线 + +探针本体见 `App/util/task_prof.h`(DWT `CYCCNT`,168 周期 = 1 µs;每 ID 记 max/count/last, +enter/exit 段还能读出"此刻仍在段内已多久"——**永久阻塞的段永不结算,只有这个字段能证明**)。 + +| 字段 | 探针 | 含义 | 基线(2026-10-10 v2.8,无客户端,开机 29 s) | +|---|---|---|---| +| `uip` / `uil` | UI_PERIOD | uiTask 循环周期最坏 / 最近值 | **61.3 ms** / 10.0 ms——⚠ 地板是 **LVGL 整帧渲染**(独立实测帧周期 61.6 ms),**与网络无关** | +| `uiw` | UI_WORK | 本轮起点→心跳前(含 LVGL) | **11.4 ms**(改前 ≈50 ms 且 ≈`uis` → 当时单轮几乎全是推流) | +| `uis` / `uisn` | UI_SINK | 推流往返最坏 / 此刻仍在段内 | **1 µs** / 0(改前 ≈50 ms);**`uisn` 持续非 0 = "客户端在连且 netTask 拥塞"这一格真发生的铁证** | +| `adcm` | ADC_BEAT | adcTask 单拍(含心跳日志) | 2.0~2.25 ms——⚠ 峰值来自**每 10 s 一次的心跳日志**,不是采集,勿混读 | +| `adcp` | ADC_POLL | `adc_input_poll()` 单次 | **8 µs**(DMA 前 122 µs;超时拍曾 2.2 ms) | +| `defd` / `defs` | DEF_DRAIN / DEF_SYSLOG | 控制台排空 / syslog 落盘 | **46 ms** / 7 µs——**当前最大的单一非让出占用** | +| `netp` / `netn` | NET_POLL | `net_poll()` 最坏 / 在段内 | **0 µs** / 0——⚠ **0 是真实值**:空闲时 `net_poll()` 开头读 INT# GPIO 为高即 `break`,整条就是一次 GPIO 读,别判成"探针坏了" | +| `gl` | glitch | DWT 丢样 / 计数器重置计数 | 0(非 0 时以上数据不可信) | + +**判读口诀**:`uip` 大而 `uiw`/`uis` 小 → **被调度饿死**;`uis` 大 → **阻塞在网络往返**; +`uiw` 大而 `uis` 小 → **自身渲染拖长**。三种机理在日志上必须能分开,否则归因只能靠猜。 + +### 11.5 三条教训(同类问题会复发) + +1. **门控类优化必须保留低频兜底。** `readable` 只由 RECV_OK 中断置位,而 `net_reconcile_tick()` + 的三类对账**不含**"芯片 RX 非空但 `readable=0`" → 中断一旦丢失,纯门控会把已到达的数据 + **永久藏住**。现每 32 轮无条件 recv 一次。**拿"自愈能力"换性能是隐性负债。** +2. **门控会顺手消灭暴露路径。** 加了 accept 门控后,监听控制块消失时 `net_can_accept()` + 恒返 0 → 改前"每轮 accept 撞 BADF"这条**暴露**随之消失 → 端口永久不再接受新客户端而无声。 + 现补 `net_get_sock(g_listen) == NULL` 判定。**加"跳过"逻辑时要问:原来哪条错误是靠这次调用暴露出来的?** +3. **把"定时轮询"换成"事件驱动"时,必须单独核对节拍源启动时刻 vs 消费者创建时刻** + (本仓 TIM7 分支 2026-10-02 的实测坑同款)——否则第一次上电就踩"ISR 对还不存在的消费者动手"。 + +### 11.6 明确不做的四条(理由要留着,别被"顺手优化"推翻) + +| 备选 | 为什么不做 | +|---|---| +| **提高 uiTask 优先级** | 会**掩盖**"心跳依赖他任务"与"忙等"两个真实缺陷;而且它根本不解决"UI 在等 netTask"。缺陷已修完后,提优先级才成为可讨论项 | +| 给 `net_*` 加"有界等待"版 | 回执通道是"每任务一个 32 位通知值",超时后**迟到的回执会被下一次调用的零等待拾取当成新结果**(`net_wait_result` 注释里 L7 挂死/整段错位的同款根因)。要做对必须引入请求序号,代价高于搬家 | +| 把推流搬出 uiTask(独立 `uiSinkTask`) | 实测 `uis`=1 µs 而 `uip` 仍 60.9 ms = **LVGL 帧周期地板** → 搬走推流**不会让刷新变快**;收益只剩极端并发下的观感隔离,成本含**必改 `.ioc`** 的重生成与全套回归。若日后成问题,正确方向是先治 netTask 排队公平性(登记簿 N-4 二期),不是搬 UI | +| 把推流塞进 netTask | 零往返零新增 RAM,但共享源码要走 backend 端口(双端同改)、netTask 这个唯一执行者更胖、一个 UI 侧 bug 直接变成整机复位 | + --- ## 附录 A:参考依据(当前有效) @@ -524,7 +627,7 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建 | 来源 | 引用点 | |---|---| | `docs/芯片手册/STM32F4xx参考手册_V18英文版_1-1000.md` §21 | 12 位递减计数器、LSI 时钟源、启动后不可停、超时公式、`SR.PVU/RVU` 同步 | -| 同上 §6.3.22(`RCC_CSR`) | `PORRSTF/BORRSTF/PINRSTF/SFTRSTF/IWDGRSTF/WWDGRSTF/RMVF`;"复位标志仅由电源复位清除" | +| `docs/芯片手册/STM32F4xx参考手册_V18英文版_1-1000.md` §6.3.22(`RCC_CSR`) | `PORRSTF/BORRSTF/PINRSTF/SFTRSTF/IWDGRSTF/WWDGRSTF/RMVF`;"复位标志仅由电源复位清除" | | `Drivers/CMSIS/.../stm32f407xx.h` | `IWDG_TypeDef`(KR/PR/RLR/SR)、`IWDG_BASE`、`IWDG_SR_PVU/RVU`、`RCC_CSR_*` 位定义 | | `STM32F407-Demo.ioc` | `Mcu.IP4=IWDG`、`IWDG.Prescaler/Reload`、`RCC.LSI_VALUE=32000`、`FREERTOS.Tasks01`、两个加固宏 | | `Inc/stm32f4xx_hal_conf.h:58` | `HAL_IWDG_MODULE_ENABLED`(已启用) | @@ -534,7 +637,7 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建 | `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:备选方案取舍记录 @@ -573,3 +676,6 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建 | 2026-10-10 | **正文按当前代码重写(清理立项前的描述)** | 正文原样保留了大量**立项前(2026-09-29)**的状态取证与已废弃的"路径 C 考古",与现状矛盾,故重写为现状设计说明(1144 → 571 行;**历史与逐次决策全部保留在下方变更记录,未删**)。主要更正:① §1.1"现状:无任何硬件看门狗"改为**历史背景**(IWDG 已在 `.ioc`/`hal_conf`/工程三处就位);② **ftpTask 早已纳入监视**——经 `lftpd_task_wd_beat()` 弱钩子(事件循环 / RETR·STOR 分片 / 连接与卷锁等待),旧正文"首期不埋点、由掩码剔除"及 R3/O1 已作废;③ 补齐正文缺失的三块现状实现:**FTL 心跳路由**(`task_wd_bind()` + `nand_ftl_wd_beat()`)、**FTL 直接喂狗**(`nand_ftl_wd_feed()`,含"设备存活但功能冻结"边界)、**round 7/8/8b 取证机制**(`/log/wdev.log`、`.noinit` 槽 + `UNINIT_CCM`、`PREVRUN`/`TICKRUN`/`MISS start`/`STALL`);④ 掩码改为按代码实况列表(产品 `0x37` / `ENABLE_HW_LAYER_TESTS` `0x35` / **testTask 任何构建都不入掩码**)并写明"精确相等"语义;⑤ 复位原因优先级按 `reset_cause_str()` 实际顺序改写(旧表把 POR 排第一,代码是 **IWDG 最优先**);⑥ §4.6 更正为**已集成**:看门狗复位写 `selftest.log`(旧正文"未改");⑦ §8 预算改用 `.map` 实测(`task_watchdog.o` 1098 B 代码 / 44+104 B RAM / `.noinit` 48 B;`iwdg.o` 仅 28 B,`HAL_IWDG_Init` 94 B 被移除);⑧ 附录 B 自相矛盾修正(旧表把"借助 CubeMX"列为否决,实际已选它);⑨ §7 由"验证计划"改为**验证记录**(V2/V4/V5 ✅、U1 39 断言 ✅,V3/V6/V7 标为未单独注入、V8 未做)。连带同步:`Src/iwdg.c`、`Src/freertos.c`、`App/task/default_task.c` 三处注释的章节引用(旧 §4.5/§4.6 → 新 §4.1/§4.4),`scripts/check_cubemx_survival.py` 提示语(§10.1 → §10.2/§10.3),`实机测试实施指南.md`(§10 → §7 ×3)、`看门狗单元测试说明.md`(§10 → §7)、`模拟器差距清单.md` H-1(§4.9 → §4.7,并更正"模拟器不编译本模块、无 IWDG 桩")、`docs/README.md`("实施中/待验证" → 已闭环)。⚠️ **下方历史行里的 §x.y 引用指当时编号**,不再回溯改写 | | 2026-10-10 | **adcTask 纳入监视(B1):必需掩码 `0x37`→`0x77`** | B1(ADC 采集链路)落地时新增 `adcTask`(`App/task/adc_task.c`:1 ms 采样节拍触发 CONVST + 消费 `tpafe5160_snapshot()`)并纳入 IWDG 监视:`task_wd_id_t` 新增 `TASK_WD_ADC = 6`;`wd_required_mask()` 产品 `0x37`→`0x77`、`ENABLE_HW_LAYER_TESTS` `0x35`→`0x75`(adcTask **不依赖任何就绪屏障** → 任何构建都置位)。adcTask 主循环每 1 ms `task_wd_beat(TASK_WD_ADC)`;**不做 NAND I/O → 不登记 FTL 心跳路由**(无需 `task_wd_bind()`)。主机侧单测 `beat_all()` 与掩码断言(`tc_003`/`tc_005`/`tc_011` 的 `0x37`→`0x77`)同步,39 断言全绿。任务由 `Src/freertos.c` 的 `USER CODE` 区手工创建(同 timeSyncTask,不在 `.ioc` 里),栈 512 words、优先级 Normal,`stack_report.py` 实测最坏链 504 B(余 1544 B / 75%)。见 `docs/算法/继电保护功能复现规划.md` §7 | | 2026-10-10 | **B1 `@test` 实机闭环:adcTask 掩码真机验证** | 台架闭环(`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`)确认 `adcTask` 纳入掩码后**无 adc 相关误复位**:`[STACK] adc peak_used=496/2048 free_min=1552 (75%)`、`frames/seq` 每 10 s 严格 +10000(=1000 Hz)、`idle=0`/`frst_err=0`、全片回读 `No difference found`。启动早期两次 `MISS start snap=0x67 req=0x77` 仅缺 bit4(`timeSync` 未启动,属正常就绪窗;adc 的 bit6 自始置位),掩码随后收敛、全程无复位。同轮另修 `dbg_console_writer_ready()` 门控(adcTask 周期日志在写者注册前抑制,见 `控制台单写者日志设计说明.md`) | +| 2026-10-10 | **新增 §11:任务级饥饿的诊断方法与实测基线** | 承接 Task #32(`task stall: mask=0x67 req=0x77`,缺 `TASK_WD_UI`)结案后的沉淀——原分析文档 `docs/问题记录/UI任务看门狗停摆分析.md` 按"完成即销项"删除,可复用部分并入本文:**§11.1** 三条易误读的判读口径(启动期 `MISS start 0x67` 良性、`miss=` 是累计不是连续、`reset cause` 不能区分 IWDG 与台架);**§11.2** 诊断四步(先看掩码缺几位 → 量化占空比 → 用计数速率反证空转 → 用 `[PROF]` 指认占用者;本例否掉"CPU 饱和",实测 10%~13%);**§11.3** 三条结构性脆弱点(uiTask 唯一 BelowNormal/心跳依赖 netTask 往返/自定截止期不可恢复)与两个顺带修掉的**正确性**缺陷(无新帧仍压环 ≈3% 频率失配、失败拍静默沿用旧值);**§11.4** `[PROF]`/`[PROF2]` 全字段语义 + v2.8 基线(含 `netp=0` 是真实值、`adcm` 峰值来自心跳日志、`uip` 地板 = LVGL 帧周期)与三机理判读口诀;**§11.5** 三条教训(门控须留低频兜底、门控会消灭暴露路径、事件驱动要核对节拍源与消费者创建时刻);**§11.6** 明确不做的四条及理由。§9.1 的 R7 行改指本文 §11.3 | +| 2026-10-11 | **新增 §10.4:ARMCC `__asm` 的三条语法约束** | AGENTS.md 瘦身:「ARMCC 内联汇编语法」条目的完整细节(GCC 语法报 `#18`、块内联把寄存器名当 C 标识符报 `#20`、`__asm` 取不到 C 数据符号 `A1516E`、可用写法 = 整函数 `__asm void f(void)` + `B` 到 C 入口、`#define f f_Gen` 让位机理)迁入本文,AGENTS.md 只留一行禁令与指针 | +| 2026-10-12 | **iec104Task / protectTask 纳入监视(B4/B5):必需掩码 `0x77`→`0xF7`→`0x1F7`** | 两行合并记录(B4 落地时漏补本文,B5 一并纠偏):**B4** 新增 `TASK_WD_IEC104 = 7`(iec104Task 100 ms 服务循环喂位,NET_READY 屏障等待按 100 ms 分片喂位),产品 `0x77`→`0xF7`、HW 层 `0x75`→`0xF5`;**B5** 新增 `TASK_WD_PROTECT = 8`(protectTask 20 ms 判据节拍喂位,见 `docs/算法/继电保护功能复现规划.md` §7),产品 `0xF7`→`0x1F7`、HW 层 `0xF5`→`0x1F5`。两任务均**不依赖存储/网络之外的屏障**(protectTask 无任何屏障)→ 任何构建都置位;均不做 NAND I/O → 不登记 FTL 心跳路由。主机侧单测 `beat_all()` 与 `tc_003`/`tc_005`/`tc_011` 掩码断言同步至 `0x1F7`,39 断言全绿。⚠ 注意 `0x1F7` 不含 bit3(testTask 任何构建不入掩码,口径不变) | diff --git a/docs/子系统设计/网络Socket监听生命周期设计说明.md b/docs/子系统设计/网络Socket监听生命周期设计说明.md new file mode 100644 index 00000000..18f92d38 --- /dev/null +++ b/docs/子系统设计/网络Socket监听生命周期设计说明.md @@ -0,0 +1,547 @@ +# 网络 Socket 监听生命周期设计说明(B10 根治) + +> 定位:回答"为什么 CH395F 上监听会被毁、端口会永久拒连",并给出根治路线。 +> 事实来源:`docs/芯片手册/CH395F.md`、`docs/问题记录/CH395F_Trap_Records.md`、 +> `Drivers/BSP/NET/net_socket.c`(3921 行)、官方 CH395EVT V2.0 例程(主机侧核对)。 +> 本文只定原理与方案,不含已落地代码;落地按 §5 分阶段,每阶段独立可回滚。 + +## 1. 一句话根因 + +**监听器是一个长期服务对象,却被实现成一个短暂的芯片 Socket;而"这条会话结束了"这个事实, +是用一个对时序敏感的边沿来通知的。** + +由此必然派生两类事故:① 没人接住边沿 → 监听器无人重建(B10 孤儿监听); +② 别人抢先接住 → 消费者永远收不到结束信号(会话卡死、新连接被旧会话静默吸收)。 + +## 2. 原理:三层证据 + +### 2.1 芯片层——"监听"不是能力,是一个会被消耗掉的槽位状态 + +| 事实 | 出处 | +|---|---| +| LISTEN 与 ESTABLISHED 是同一 TCP 状态字节的**互斥取值**(01H/04H) | `docs/芯片手册/CH395F.md` §5.22 L535-545 | +| 「Socket 会一直检测连接事件,**直到连接成功后**……每个 Socket 只可以建立一个连接」 | `docs/芯片手册/CH395F.md` §5.29 L609 | +| 连接结束后芯片**永不**自动回 LISTEN——全手册无此条款;重连必须"重新打开 Socket 再 CONNECT" | `docs/芯片手册/CH395F.md` §5.30 L615、§9.2.5 L1266 | +| 断开后芯片行为由 FUN_PARA bit3 分流:=0 芯片自动关 Socket(→`0x00/0x00`);**=1 芯片什么都不做(→`0x05/0x00`),MCU 必须自己 CLOSE** | `docs/芯片手册/CH395F.md` §5.23 L572、§5.52 L795② | + +⚠ **本仓 `ch395f_set_fun_para(0x08)`(`Drivers/BSP/NET/net_socket.c:275`,bit3 SOCKET_CLOSE=1, +Trap 18/25 定案为保留排空现场)⇒ 断开后芯片必然停在 `0x05/0x00`。 +B10 探针看到的"孤儿芯片态"不是芯片异常,而是本仓这个配置的**直接且合法的后果**。** + +⚠ 口径错误待修:`Drivers/BSP/NET/net_socket.c:3627` 与 `:3641` 的函数头注释写 +「CH395F 在 DISCONNECT 后自动关闭 Socket(FUN_PARA 默认)」——与实际 `0x08` 相反。 + +### 2.2 规范层——本仓的做法与 TCP/POSIX 规范正面冲突 + +- RFC 1122 §4.2.2.18:被动 OPEN「**MUST NOT affect any previously created connection record**」。 +- POSIX `accept()`:「The original socket remains open and can accept more connections」。 +- lwIP 把"监听器归谁"直接写进 `tcp_close()` 文档:只有 socket 层创建的监听器才会被连带关闭, + raw API 自建的不会——**所有权是 API 契约的一部分,不是调用方的自觉**。 + +本仓让同一个 fd 兼任"监听器"和"连接"两个角色(handle/resource aliasing), +于是"关掉一条连接"和"毁掉一个监听器"在类型上无法区分—— +这正是 AGENTS 里「应用层不得在 EOF 时 `net_close` 该 fd」这条铁律存在的唯一原因。 +**需要靠铁律维持的正确性,就是架构欠债的凭证。** + +### 2.3 实现层——别名导致"边沿通知",进而导致多所有者 + +`net_recv` 的 EOF 只在 **recv 那一刻 `state==CLOSED`** 才给出 +(`Drivers/BSP/NET/net_socket.c:1880-1888`);若该 fd 已被别的路径 relisten 回 LISTENING, +同一个 recv 恒返 `WOULDBLOCK`(`:1890`)。这是一个**边沿**,不是一个**电平**。 + +于是"谁重建监听"散成 **12 个所有者**:栈内 8 处(对账③④、`handle_disconnect_event` 监听分支、 +`net_accept_locked` CLOSED 分支、`net_recv_locked`、`net_pending_tick`、`NET_MSG_RELISTEN`、 +`net_resync_listen`、`net_listen_locked`)+ 应用层 4 处(lftpd、time_sync、ui_net_sink、net_task 代次)。 +状态迁移点 15 个,其中 **15 个"无人负责"组合**(U1~U15,见 `docs/问题记录/CH395F_Trap_Records.md` B10 行)。 + +三个已实测的派生失效: + +| 失效 | 机理 | 现状 | +|---|---|---| +| 孤儿监听(B10) | 边沿没人接,四方互等 | 对账④兜底 ≈1 s(已提交 `4f2cc8b`) | +| 消费者卡死 | ④/D10 抢先 relisten → recv 永久 `WOULDBLOCK` → `g_client` 不清、新连接被旧会话静默吸收 | **④ 扩大了此窗口**,未修 | +| 监听被误关 | `net_can_read` 在非 ESTABLISHED(含 LISTENING)返 1 → recv 得 `WOULDBLOCK` → `Drivers/BSP/UI/ui_net_sink.c:563` 无护栏 `net_close(g_client)` = 关掉监听 fd | 靠随后 `bind_listen` 自救,落 Trap 09 沉降窗 | + +另有一条**探针失效**:`net_can_accept`(`:1453`)要求 ESTABLISHED 才返 1, +而 `Drivers/BSP/UI/ui_net_sink.c:512` 用它门控 accept → :7003 **永不进 `net_accept`** +→ Trap 25 的 `net_lost_connect_probe`(`:1250-1289`)对这个端口**从不运行**, +丢 CONNECT 只剩对账②。:21/:8888 因为无条件轮询 accept,探针才活着。 + +### 2.4 FUN_PARA bit3=1 的必要性与安全边界(2026-10-12 评估,结论:**必须保留 0x08**) + +> 用户提问:「bit3 在本项目设置为 1 的必要性大吗?会导致这么多问题;如果设为 1, +> 如何安全处理?」本节以手册原文 + 实测证据回答,并把它并入决策记录 D4。 + +**① 必要性 = 硬性必需,不是可调项。** 手册对 bit3 的分流写得很直白: + +- `docs/芯片手册/CH395F.md` §5.52 注释②:「此位为 0 表示在产生 SINT_STAT_DISCONNECT 或者 SINT_STAT_TIM_OUT 中断后, + CH395 会主动关闭 Socket。……**关闭 Socket 就意味着 CH395 会将此 Socket 内部缓冲区和 + 一些相关变量清除**,在某些应用中,如果 CH395 内部自行关闭 Socket 可能会导致残留的数据 + 被强行清除。这种情况下可以将该位置 1,**由单片机将数据读出来,然后再关闭 Socket**。」 +- `docs/芯片手册/CH395F.md` §5.23 L572 同向:「为 0 则……主动将 Socket 状态置为关闭状态**并将相关缓冲区全部清除**; + 反之则不会对 Socket 状态和相关缓冲区做任何操作,以方便外部单片机……**将残留的数据读出**。」 + +实测现场(Trap 25 三次定案的判别探针):`st=0x05/0x00` 且 `recv_len=1027`—— +Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少**。这正是 bit3=1 按规格 +保留现场,不是芯片异常(`docs/问题记录/CH395F_Trap_Records.md` §877-878)。 + +**② bit3=0 的代价是「硬件保证的数据丢失」,且不可在软件层补救。** +本产品最典型的流量形态是「发完即 FIN」:ftplib `storbinary` 语义 = `sendall` 发完立即关 +数据连接、回控制通道等 226。芯片把最后一段数据落进 RX 的同时抛出 DISCONNECT: + +- bit3=1:RX 数据在,MCU 来得及读(Trap 23 的「排空 RX 再 close」修复才成立); +- bit3=0:芯片**当场**关 Socket 并清缓冲 → MCU 永远读不到尾部 → 文件被截断却仍回报 226 + (静默损坏)。这是 bit3=0 的**必然结果**,不是概率问题——软件再聪明也拿不回已被硬件擦掉的数据。 + +即:改 bit3=0 等于用「一个硬件保证、不可修的数据丢失」去换「一个软件侧可修的关闭义务」, +严格更差。:8888 对时包、:7003 远显推流同样是「数据 + 随即关闭」形态,一并受害。 + +**③ bit3=0 也并不能真正消除孤儿态。** 芯片自动关以**收到** DISCONNECT/TIM_OUT 为前提, +而 Trap 22 已实证快速复连场景芯片会**丢**这两个中断(`docs/问题记录/CH395F_Trap_Records.md` +B10 行)→ 中断丢了,bit3=0 同样停在已关/半关态,孤儿 Socket 照样存在。 +它只是降低出现概率,代价却是上面第②条。 + +**④ 所以「这么多问题」不归 bit3=1。** 断开后停 `0x05/0x00` 是 bit3=1 的**合法后果**(D4); +真正造成 B10 永久拒连、消费者卡死、EOF 被吞没的,是软件侧没把「该由 MCU 关 Socket」这条 +义务落到唯一所有者——重建所有者 12 个、无人负责组合 15 个(§2.3),以及 U12 的「relisten +的 LISTEN 失败不 close 芯片 Socket」与 open 失败路径不对称。bit3=1 只是把义务显式化, +把原本被芯片掩盖的架构欠债照了出来。 + +**⑤ 安全处理 = 把 bit3=1 的契约在软件侧收口**(即 §4 的两条不变量,逐条对应上面的缺口): + +| 契约要求 | 对应缺口 | 收口手段 | +|---|---|---| +| 「会话结束」必须被**谁都能看见**,不能只在边沿那一刻 | EOF 边沿化:抢先 relisten → recv 永久 `WOULDBLOCK`(§2.3) | 不变量 I:`net_sock_t.session_dead` 粘滞电平,`net_recv_locked` DONTWAIT 判据改 `state==CLOSED \|\| session_dead`(P1) | +| 关闭 Socket 必须**一定有人做**、且只做一次 | 重建所有者 12 个 + U12 的 LISTEN 失败不回 close | 不变量 II:`net_listener_pump` 单点重建 + 补 U12 的 close 对称(P2) | +| 关闭姿势必须正确 | Trap 14/17:陈旧 DISCONNECT 锁存秒断下一条连接 | `net_close_locked` 既有「close → 等 `CLOSED` → 排空本 Socket 中断」,是 bit3=1 下唯一正确的关闭序列 | +| 异常释放须先排空残渣再关 | Trap 23:~2KB RX 残渣被下条连接当自己的数据读走 | `net_relisten_locked` 隔离期(400 ms)持续排空 RX + 主动 close + 等 `CLOSED/TCP_CLOSED` | + +**⑥ 本次(P0)已落地的口径修正**:`net_socket.c:3627` / `:3641` 原注释写「DISCONNECT 后 +芯片自动关闭 Socket(FUN_PARA 默认)」——与实际 `0x08` **相反**。这类注释正是 U12 这类 +「跳过显式 close」缺陷的观念源头,已改正并留痕。 + +## 3. 为什么不能走"芯片原生多连接模式"(A′) + +芯片确实自带 lwIP 式模型:FUN_PARA bit1(≥0x44,本仓实测 0x4A)下 +「服务器的 Socket **仅用来监听**,单片机需要分配其它 Socket 用于该连接」(§9.2.6 L1295)。 +但本产品**不可用**,三条硬证据: + +1. **不支持多监听**——`docs/问题记录/CH395F_Trap_Records.md` 附 FTP 改造清单第 4 条 + 「CH395F 多连接模式不支持多监听(类型:架构)」,处置即 `fun_para=0x00`。 + 本产品有 21/8888/7003 **三个常驻监听** + 会话期动态 PASV 监听。 +2. 池分配按**源端口匹配**(`docs/芯片手册/CH395F.md` §9.2.6 L1295),多监听器的池会互相串。 +3. 历史实测:「多连接模式下最多接受 3 个客户端(Socket 1~3),Socket 4~7 的 CONNECT 中断从不触发」(:10)、 + 「多个 Socket 数据互相覆盖」(:35)——即 Trap 02/11。 + +且 FUN_PARA **初始化后不可改**(`docs/芯片手册/CH395F.md` §5.52 L795⑥),这是开机一次性决策,不能运行时降级。 +**结论:A′ 出局;根治必须在单连接复用模型内做。** + +## 4. 根治原理:两条不变量 + +- **不变量 I(电平化)**:「这条会话已结束」必须是**可重复查询、幂等**的状态, + 而不是"恰好在那一刻观察到才算"。谁先看到不重要,**谁都饿不着**。 +- **不变量 II(单一所有者)**:监听器的重建**只有一个决定者**(net 层的 listener pump)。 + 其它所有位置只**置事实**,不**做动作**。 + +两条不变量成立后,"谁重建""会不会错过"这两类问题在结构上消失, +对账④从"唯一救命稻草"降级为"事件丢失时的补漏"(Trap 22 实证事件会丢,故对账**必须保留**)。 + +## 5. 分阶段方案 + +### P0 口径与登记(零风险,纯文档/注释) + +1. 修 `net_socket.c:3627`、`:3641` 的 FUN_PARA 口径错误注释(实际 `0x08`,芯片**不**自动关)。 +2. 把 U11(本层 CLOSED + 芯片正在 LISTEN,④判据缺口)、U12(relisten 的 LISTEN 失败不 close 芯片 Socket, + 与 open 失败路径不对称)、U14(`NET_SOCK_STATE_TCP_CLIENT_RECONNECTING` 死枚举)、 + U15(丢 RECV_OK 无人对账)登记进 `docs/问题记录/CH395F_Trap_Records.md`。 +3. 在 B10 行补记残余:**④ 扩大了"EOF 被吞没"窗口**(见 §2.3)。 + +### P1 边沿 → 电平(小改,收益最大) + +1. `net_sock_t` 增 1 字节 `session_dead`(粘滞"本会话已结束"): + - **置位**:`handle_disconnect_event` 释放分支、`handle_timeout_event` ESTABLISHED 分支、 + 对账①(复用前者)、`net_connect_locked` 命令被拒。 + - **清零**:新的 CONNECT 成功(`handle_connect_event`)、`net_close`/`net_socket`(memset)。 + - **不清**:`net_relisten_locked`——**这正是关键**,重建监听与"应用是否已确认会话结束"解耦。 +2. `net_recv_locked` DONTWAIT:判据由 `state==CLOSED` 改为 `state==CLOSED || session_dead` + → EOF **可重复获得**,任何消费者都不会再饿着。 +3. `ui_net_sink` 三处收口: + - `:563` 真错误分支加 `g_client != g_listen` 护栏(与 `:552` 一致,MCU 上不关监听); + - EOF/错误后**不再**自行 `bind_listen`(重建交回 net 层,落实不变量 II); + - accept 门控从 `net_can_accept`(ESTABLISHED)改为新的 `net_listener_armed(fd)` + (LISTENING 或 CLOSED+auto_relisten)→ 让 :7003 重新吃到 `lost_connect_probe`。 +4. 验收(新增腿,专打"抢先 relisten"):客户端连上 :7003 → 拖住 uiTask 使 relisten 先发生 → + 断连 → 断言 sink 仍拿到 EOF、且**下一个客户端能被服务**(当前会被旧会话静默吸收)。 + 回归矩阵沿用 B10 那套(`:7003` 10/10、`:21` 50/50×2、`:8888` 20/20、`ftp_regression` 11/11、Trap 23 5/5×2)。 + +### P2 单一所有者(中改,消灭多所有者)—— P2a ✅ 2026-10-12 / P2b 建议暂缓 + +1. 新增 `net_listener_pump()`(100 ms 周期),接管**周期性**重建,是唯一做周期性 + `net_relisten_try` 的地方。✅ 已落地 +2. ⚠ **原第 2 条已被实测推翻**:原计划「删除 `net_accept_locked` / `net_recv_locked` / + `net_pending_tick` 三处即时重建」。实测删掉会把 CLOSED 窗口从 ~0 拉长到 ~100 ms, + 调用方高频空转 `net_accept`(RPC 无上界)→ uiTask 最坏单轮 172 ms → 1034 ms + `[WD] task stall`。 + **故保留这三处**(消费者动作触发,非周期性抢先),只删对账③④ 两处**周期性**路径。 + 调用点 7 → 5。✅ 已按修正版落地(详见 §9) +3. P2b:消灭 netTask 内的同步忙等(U6):`net_close_locked` 的 500 ms `osDelay` 循环、 + `net_listen_locked` 的 3×500 ms,改用既有 D10 非阻塞重试模式。 + **建议暂缓** —— 实测 `task stall` 在忙等未改造时同样存在,危害是理论性的(详见 §9) +4. 补 U11:本层 CLOSED 而芯片**正在** LISTEN → 只做状态重同步,不重下发命令。✅ 已落地 +5. 验收:`:21` 0ms×50 仍 50/50;`netp`(`net_poll` 段最长耗时,µs)不劣化。✅ 见 §9 + (⚠ 原判据「close/listen 风暴期间无 `[WD] task stall`」**不可用**:该现象在基线同样存在, + 见 §9 的 P1/P2a 对比数据) + +### P3 远期(**不建议现在做**,仅登记方向) + +形态 A:listener 成为一等对象 + `accept` 返回虚拟句柄(与 `simulator/sim/net_socket_sim.c` +已是同一模型,消除真机/模拟器双模型分裂)。代价:控制面重写 15~25 函数、RAM +<1 KB、Flash +2~6 KB、 +回归面覆盖全部服务与板级套件。只有当产品需要"同一端口并发多连接"时才值得立项。 + +## 6. 顺序与回滚 + +P0 → P1 → P2 严格串行,每阶段独立提交、独立回归、可单独回滚。 +P1 是性价比最高的一步:它不改变 fd 模型,却把 B10 与"消费者卡死"两类事故同时从结构上消掉。 +P2 之后,对账④保留但只作事件丢失的补漏(Trap 22 实证事件会丢,不可删)。 + +## 7. 证据附录(2026-10-12 实测与调研,供后续执行时复核) + +### 7.1 B10 现场与修复实测 + +| 项 | 数据 | +|---|---| +| 孤儿态探针(临时,测完已删) | `TEMP orphan sock2 state=0 port=7003 chip=0x05/0x00` 每秒一条持续 12 s+;`:7003 300ms×10` → `SUMMARY rounds=10 ok=0 fail=10 unrecovered=10`;+30 s 仍拒连 | +| 判"是否 B3 引起"的 A/B | 基线 `794a688` = 5/1/1/5(均值 3.0)vs B3 构建 = 3/7/2/1(均值 3.25)→ **统计不可区分**;每个样本前硬复位 + 等 12 s 干净启动 | +| ④ 修复后 | `:7003 300ms×10` **10/10**(+20 s 存活);`:8888 0ms×20` 20/20;`:21 0ms×50` 50/50;`:21 100ms×50` 50/50;`ftp_regression` 11/11 ×2;`ftp_abort_probe --check-abort` RST 5/5、`--graceful` FIN 5/5;`@test` PASS(600 ms、回读无差异);`Code` 197008→197084 | +| 救援日志(生效证明) | `sock2 reconcile: orphan listener (chip=0x05/0x00) -> relisten` → `auto-relisten ok` / `relisten retry ok` | +| 台架方法论坑 | ① 一轮把监听打死不自恢复 → **每腿前必须硬复位**,否则 A/B 被污染(本次第一腿就是这么废的);② 判固件回归前先核对主机侧工具凭据(`ftp_abort_probe.py` 用 anonymous 登录,N-19 后 530 拒 → 假 0/5) | + +### 7.2 官方例程的所有权模型(CH395EVT V2.0,主机路径 `D:\Code\DTU\使用的芯片手册\CH395EVT\EVT\`,不在仓内) + +- **EXAM5(多连接)**:`main.c:296 SetFunPapr(0x02)`(bit1=1);Socket0 `OPEN+TCPListen` 后再不碰(:117-127),Socket1~7 只配端口/协议**不 OPEN**(:129-133);DISCONNECT/TIM_OUT **只打印、不 close 不重 LISTEN**(:203-212)——恢复完全由芯片自动,**只在多连接模式成立**。 +- **EXAM10(单连接,FTP Server)**:DISCONNECT → `CH395.C:234` 显式 `CloseSocket` → `:237-238` **立即重新 OPEN + 重新 TCP_LISTEN**;TIM_OUT 同路径(`:250`/`:254`);调用者是中断处理器;close→reopen **无延时无重试**,失败 `mStopIfError` 挂死。 +- **结论**:单连接模式下官方**零自动重监听**,且原生做法是**在事件现场当场重建**;本仓对账④是周期兜底,方向一致但不可互替(Trap 22 实证事件会丢)。 +- **中断所有权**:全局寄存器整体读即清、无按位清除(`docs/芯片手册/CH395F.md` §5.15 L441);CONNECT/DISCONNECT/TIM_OUT 是**事件位**(可丢)、RECV 是**条件位**(可补)→ 任何"只靠事件重建监听"的方案天然不完备。 + +### 7.3 状态机事实(`Drivers/BSP/NET/net_socket.c` 3921 行) + +- `->state =` 赋值 **13 处**(:815 :953 :1069 :1072 :1139 :1685 :1693 :2075 :3577 :3611 :3698 :3765 :3787)+ 2 处 memset(:249 全表、:2622 单块);`state` 的**全仓唯一写者**是本文件。 +- **重建所有者 12 个**:栈内 8(对账③④、`handle_disconnect_event` 监听分支、`net_accept_locked` CLOSED 分支、`net_recv_locked`、`net_pending_tick`、`NET_MSG_RELISTEN`、`net_resync_listen`、`net_listen_locked`)+ 应用层 4(lftpd、time_sync、ui_net_sink、net_task 代次)。 +- 做决策必须知道的判据:`net_listen_locked:1102-1104` 状态门**只允许 BOUND/CREATED**(对已 CLOSED 的监听 fd 直接 `net_listen` 返 INVAL);`net_connect_locked:1583` **无状态门**(可在 LISTENING 上改成 CONNECTING);`net_recv_locked:1862-1873` 快路径**无状态门**;`net_can_accept:1453` 要求 ESTABLISHED。 +- M6(`handle_disconnect_event:3659-3701` 置 CLOSED)之后,**唯一不依赖应用调用的收口是对账④**;`:1346`/`:1884`/`:2451` 三条内部路径全部以"应用先发起一次 accept/recv"为前提,而 `ui_net_sink.c:512`/`:536` 的门控恰好把这两类调用在 CLOSED 态下全部掐断——这就是四方互等的成因。 + +### 7.4 三个服务的契约互不一致 + +| 服务 | 认为的连接结束信号 | net 层实际给什么 | 监听重建者 | 是否留无人重建组合 | +|---|---|---|---|---| +| `lftpd` 控制 :21 | `read_line!=0`(EOF→-1,`lftpd_inet.c:154-156`) | 阻塞 pending 保证 CLOSED 必了结(FIN→0 / 异常→CONNRESET,`net_socket.c:2457-2462`) | net 层(`:2449-2451`),应用**永不** close 控制 fd(`lftpd.c:746-750`) | 无(契约一致,是正面样板) | +| `time_sync` :8888 | EOF/30 s 预算/错误,随后**无条件** `net_close(conn==ls)`(`time_sync.c:182`) | 同上 | 应用自己每会话全量重建(`:224`) | 不留孤儿,但违反「EOF 不得 close 监听 fd」→ close→重建沉降窗(Trap 09)+ 最坏 ~10 s 端口下线 | +| `ui_net_sink` :7003 | `net_recv` 返 0 或真错误;重建条件 `net_get_sock(g_listen)==NULL`(`:557`/`:566`) | 该 0 只在 recv 时刻 `state==CLOSED` 才给 | 名义 net 层,实际自身条件在 MCU **恒假**(fd 不释放) | **有**(B10 与"消费者卡死"都出自这里) | +| 模拟器后端 | `recv` 0 恒可得、fd 分离(`net_socket_sim.c:293-336`) | Winsock 原生 | 无需 | 无——且对上述全部失效模式**零覆盖** | + +### 7.5 ④ 副作用的判据链(P1 要消除的对象) + +`net_can_read:1477-1478` 在非 ESTABLISHED(**含 LISTENING**)返 1 → 消费者去 recv → +DONTWAIT 在 LISTENING 下返 `WOULDBLOCK`(`:1890`)→ `ui_net_sink.c:560-569` 判为"真错误"→ +`:563` **无 `g_client != g_listen` 护栏**的 `net_close(g_client)` 在 MCU 上等于**关掉监听 fd** +(与同文件 `:134` 注释"严禁"自相矛盾)→ 靠随后 `bind_listen` 自救,落 Trap 09 沉降窗。 + +## 8. 决策记录 + +| # | 决定 | 依据 | +|---|---|---| +| D1 | **A′(启用芯片多连接模式)出局**,根治必须在单连接复用模型内做 | `docs/问题记录/CH395F_Trap_Records.md` 附 FTP 改造清单第 4 条「多连接模式不支持多监听」;本产品 3 常驻监听 + 动态 PASV;§9.2.6 L1295 池按源端口匹配会互串;历史实测 Trap 02/11;FUN_PARA 初始化后不可改(`docs/芯片手册/CH395F.md` §5.52 L795⑥) | +| D2 | 采用**形态 B 深化**(电平化 + 单一所有者),**不做 P3** | P3 需控制面重写、RAM/Flash 上升,只有"同端口并发多连接"需求才值得 | +| D3 | 对账④**保留**(`4f2cc8b` 不撤回),其副作用由 P1 消除 | Trap 22 实证事件会丢 → 周期对账不可删;④ 已把 `:7003` 从 0/10 永久死拉到 10/10 | +| D4 | `FUN_PARA` **保持 `0x08`**,不因孤儿态改配置(2026-10-12 复核确认) | bit3=1 是 Trap 18/25 定案的"保留排空现场"契约;bit3=0 会令芯片在 DISCONNECT/TIM_OUT 时**当场清缓冲**(`docs/芯片手册/CH395F.md` §5.52② / §5.23 L572)→「发完即 FIN」流量的尾部数据必被硬件擦除、**不可补救**;且 bit3=0 也挡不住丢中断(Trap 22)→ 孤儿态照样有。孤儿态是 bit3=1 的合法后果,应在软件层收口而非回退契约。完整论证见 §2.4 | +| D5 | 顺序 **P0 → P1 → P2**,每阶段独立提交、独立回归、可单独回滚 | P1 不动 fd 模型却消掉两类事故,性价比最高 | +| D6 | 真机/模拟器**双模型分裂暂不消除** | 消除即等于做 P3;先靠 P1 把应用侧契约对齐 | +| D7 | 同族残留**不在本次根治范围**,各自单列:U1(CONNECTING 滞留,B2 残余)、U11、U12、U14、U15,以及审计项 B4/B5/B6/B7/B8/B9 | 与监听生命周期不同因,混做会让回归面失控 | + +## 9. 执行清单(后续按此执行,逐项勾选) + +**P0(零风险)** +- [x] 修 `Drivers/BSP/NET/net_socket.c:3627`、`:3641` 的 FUN_PARA 口径错误注释(实际 `0x08`,芯片**不**自动关)—— 2026-10-12 已改并留痕(§2.4⑥),`@build` 0 错 0 警 +- [ ] 把 U11 / U12 / U14 / U15 登记进 `docs/问题记录/CH395F_Trap_Records.md` +- [ ] 在 B10 行确认已记「④ 扩大 EOF 吞没窗口」(已记,执行时复核) + +**P1(边沿→电平)** +- [x] `net_types.h` 的 `net_sock_t` 增 `uint8_t session_dead;` +- [x] 置位:`handle_disconnect_event` 释放分支、`handle_timeout_event` ESTABLISHED 分支、对账①(复用前者)、`net_connect_locked:1685` +- [x] 清零:`handle_connect_event`(新 CONNECT,两支)、`net_close`/`net_socket` 的 memset 与显式清零;**`net_relisten_locked` 不清**(关键,已加注释钉住契约) +- [x] `net_recv_locked` DONTWAIT 判据改 `state==CLOSED || session_dead` +- [x] `ui_net_sink.c:563` 加 `g_client != g_listen` 护栏;EOF/错误后不再自行 `bind_listen` +- [x] 新增 `net_listener_armed(fd)`(LISTENING 或 CLOSED+auto_relisten),替换 `ui_net_sink.c:512` 的 `net_can_accept` 门控(MCU + `net_socket_sim.c` 双端实现) +- [x] **超出清单的必要扩展(设计时未预见)**:`net_pending_tick` ③ 断连判据同步改 `CLOSED || session_dead`。不改则 lftpd 的**阻塞** recv 仍会被抢先 relisten 饿死——①要求 ESTABLISHED、③要求 CLOSED,抢先 relisten 后两头落空,只能干等 deadline 超时。判据放宽后,两处 relisten 调用均须补内层 `state == CLOSED` 门:`net_relisten_try` 无状态门,对已 LISTENING 的 fd 再 open+listen 会因 USE 错误走进关闭分支、**关掉正听着新 SYN 的 Socket**(U12 同源事故) +- [x] 全量回归矩阵(§7.1 那一套)—— 2026-10-12 真机全绿,见下「P1 台架验收记录」 +- [!] 「抢先 relisten」腿 —— **外部注入不可构造,已放弃**:见下「验收腿的否定结论」 +- [x] `@test` PASS → 回读校验(`No difference found`) + +**P1 台架验收记录(2026-10-12,`@build` Code=197096 / 基线 197084,+12 B)** + +| 腿 | 结果 | 说明 | +|---|---|---| +| `@test` 启动闭环 | **PASS** | 启动锚点齐、无 `[ERR]`、reset cause ×1、7.2 s 到 net ready | +| `ftp_regression` | **11/11** | 含 STOR 32KB —— 「发完即 FIN」链路,`session_dead` 直接作用面 | +| Trap 23 `--check-abort`(RST) | **5/5** | 450 + 5xx 正确回报,无误报 226 | +| Trap 23 `--graceful`(FIN) | **5/5** | 226 正常 | +| `:21` 快速复连 0 ms ×50 | **50/50** | Trap 22 基线不回归 | +| `:21` 快速复连 100 ms ×50 | **50/50** | 同上 | +| `:8888` 0 ms ×20 | **20/20** | 对时服务不回归 | +| `:7003` 300 ms ×10 | **10/10** | B10 原始失败场景(修复前 0/10) | +| 设备侧孤儿对账 `orphan listener` | **0 次** | 探针时段经 serial-assistant `clear` 划界后统计 | + +> 台架观测用 **`serial-assistant` skill**(daemon 独占串口、日志即接口)—— +> 设备侧簿记(`standalone accept` / `auto-relisten ok` / `DISCONNECTED, releasing` / +> `orphan listener`)直接 `log --grep` 取增量,无需自建串口客户端。 +> ⚠ 自建 pyserial 客户端会撞 `PermissionError(拒绝访问)`:daemon 独占绑定串口。 + +**验收腿的否定结论(重要,勿再重复踩)**:设计文档原计划的新增腿是 +「连上 :7003 → 拖住 uiTask 使 relisten 先发生 → 断连 → 断言 sink 仍得 EOF」。 +2026-10-12 实现后发现**真机外部无法构造**,且据此写的探针**无判别力**: + +- 外部唯一可注入的是「A 发完数据立刻 FIN + 立即 B 抢占」。实测该形态下 + **P1 版与对照组(撤掉 `g_client != g_listen` 护栏)双双 PASS**,设备侧 + `accept/relisten/disconnect` 簿记与 `orphan` 计数完全一致 → 探针测不到它声称要测的东西。 +- 机理:真实「抢先」要求 relisten 发生在**消费者下一次 recv 之前**;而对账④ 周期 + 200 ms + 去抖(`NET_RECONCILE_STABLE_N`),`ui_net_sink` 轮询 100 ms —— + 正常时序下消费者总是先 recv 到 EOF,对账抢不到。**只有 uiTask 被拖住**才可能出现, + 而那是设备内部状态。 +- 处置:按 Trap 25「不留未证明的复杂度」**删除该探针**;该场景登记为**残余验证项**, + 需设备内注入点(参照 `TEST_LATENCY_PROBE` 的编译开关模式)才能在真机复现。 +- 因此 P1 的**正确性依据**是:① 代码级论证(判据由边沿改电平、置位/清零完备、 + 两处 relisten 补状态门);② 上表全量回归矩阵**不回归**;③ 不变量 I 的结构性论证。 + 「抢先」的前瞻场景**未被真机证明**,如实标注,不冒充已验收。 + +**P1 落成状态(2026-10-12)**:代码已实现、`@build` 0 错 0 警(`Code=197096`,较基线 +12 B)、 +`mem-numbers` 快照已 `--write` 刷新、除下述基线问题外静态门禁全绿。 +⚠ **已知基线问题(非本改动引入)**:`doc-links` 在**任何从当前 `v2-on-ui` 切出的工作树**上都会报 +2 处失效引用——`RAM使用分析与优化方案.md` 引用了两个**尚未提交**的文件(RAM 基线 `.tsv` 与 +RAM 预算台账 `.md`,属另一在途任务)。这两个文件在主工作树磁盘上存在故主线门禁呈绿, +工作树里没有故必报红(机理见 `docs/环境与部署/并行任务工作树方案.md` §10.2)。 +待该任务合入后自动消解,**不应**由本分支代为提交他人半成品。 + +**P2(单一所有者)—— 已拆为 P2a / P2b** + +**P2a(✅ 2026-10-12 落地,提交 `7cb5af0`)** +- [x] 新增 `net_listener_pump()`(100 ms 周期 + 去抖 + 冷却),接管**周期性**重建, + 覆盖:① LISTENING 失位 ② CLOSED 不在 LISTEN ③ CLOSED 但芯片**正在** LISTEN(U11) +- [x] 删对账③④ 及其孤儿态认领门槛(对账只剩①②;认领收窄为 ESTABLISHED/LISTENING) +- [x] 补 U11:本层 CLOSED 而芯片正在 LISTEN → **只做状态重同步**(含 `last_peer_port` + 转登 + `lost_conn_seen` 清零,同 `net_resync_listen`),不发任何芯片命令 +- [x] `net_listener_armed` 收紧:CLOSED 态仅在 `readable≠0`(有残留)时放行 +- [x] 验收:`:21` 0ms×50 / 100ms×50 各 **50/50**;`:8888` **20/20**;`:7003` 300ms×10 + **10/10**;`ftp_regression` **11/11**;Trap23 abort/graceful 各 **5/5**; + 设备侧 `orphan listener` 增量 **0**(P1 基线同条件为 **2**)→ 目标达成 + +- [ ] **P2b(同步忙等改造)—— 建议暂缓**:`net_close_locked` 的 500 ms `osDelay` 循环、 + `net_listen_locked` 的 3×500 ms。**实测依据**:全量历史 `task stall` 在忙等未改造时 + 同样存在(P1 基线同条件 53 条),忙等的危害是**理论性**的;而 `net_close` 是每次会话 + 结束都走的热路径。按 Trap 25「不留未证明的复杂度」,为一个未发生的故障去动热路径, + 性价比存疑 —— 建议等有 `task stall` 与忙等的**因果证据**后再做。 + +> ⚠ **P2a 与设计文档原清单的关键差异(实测推翻原计划,务必据实执行)**: +> 原清单第 2 条要求把 `net_recv_locked` / `net_pending_tick` / `net_accept_locked` +> **三处即时重建一并删除**。台架实测证明**删掉会引入回归**: +> - CLOSED 窗口从 ~0 拉长到 pump 周期 + 隔离期(~100 ms); +> - 期间调用方高频发起 `net_accept`(RPC:入队 + `xTaskNotifyWait` 回执**无上界**) +> → 队列积压 → 实测 uiTask 最坏单轮 **172 ms → 1034 ms**,`[WD] task stall` 出现; +> - 故**保留这三处**:它们是「**消费者动作触发**」(消费者正在等这条连接的结局), +> 不是周期性「抢先」;真正该收敛的只有**周期性**路径(对账③④)。 +> - 收敛后 `net_relisten_try` 调用点 **7 → 5**(pump + 显式消息 + 3 处消费者驱动)。 + +> ⚠ **如实记录的观测**:`task stall` 是**既有现象**(P1 基线同条件 53 条 vs P2a 32 条, +> 未恶化);uiTask 最坏单轮的历史最大值 P2a 偏高(1054 ms vs P1 172 ms),但 `uil` +> (最近一次)为 10999 µs 即**当前正常**,判为一次性卡顿,**留待观察**。 + +**回滚点**:P0 纯注释可直接 revert;P1 若回归失败 revert 单提交即可(④ 仍在,退回到"端口活着但慢消费者可能卡死"的已知状态);P2a 独立成提交(`7cb5af0`),revert 后回到"对账③④ 兜底 + 12 所有者"的 P1 状态;P2b 未做。 + +--- + +## 10. 监听生命周期模块化边界设计(**已执行 2026-10-12**,提交 `c1ab902`) + +> **动机**:真机 net 层 `net_socket.c` 4126 行,而模拟器做同样的事只有 807 行(5 倍); +> `relisten` 一词在文件里出现 110 次。用户诉求 = **高内聚低耦合**。 +> P2a 把「周期性重建」收敛到 `net_listener_pump`(所有者 7→5)之后,搬移边界才**第一次清晰**—— +> 这正是 P2a 相对"直接抽模块"的价值:先收敛,再搬移。 +> +> 本节只定**边界与接口**,不含任何代码;执行须过 §10.6 的验证门。 + +### 10.1 目标与可验证判据 + +「高内聚」在本项目落成**三条可机械检验**的判据(不是感觉): + +| # | 判据 | 现状 | 目标 | +|---|---|---|---| +| 1 | 模块外调用「重建监听」的**入口数** | 5(pump / 显式消息 / recv / accept / pending) | **1**(新模块导出) | +| 2 | `s_relisten_*` 静态状态被**模块外直接读写** | 2 处外部写 | **0** | +| 3 | 依赖方向 | 无 | 新模块 → `net_socket.c` **单向**,无反向 include | + +⚠ **明确不承诺**:本重构**不消除 fd 别名**(同一 Socket 既当监听器又当连接)。 +那是 CH395F 单连接模型 + bit1 多连接模式**不支持多监听**(Trap 记录改造清单第 4 条)锁死的, +属硬件上限(§3 D1)。重构只提升**组织度**,不改变架构上限——**别指望补丁消失,只指望补丁不再重复**。 + +### 10.2 现状清点(2026-10-12,`net_socket.c` 4126 行) + +| 类别 | 数量 | 明细 | +|---|---|---| +| 纯监听生命周期函数 | **7** | `net_relisten_locked`(139) / `net_listener_pump`(107) / `net_relisten_retry_tick`(47) / `net_relisten_try`(45) / `net_drain_chip_rx`(32) / `net_relisten_clear`(28) / `net_relisten_done`(16) | +| 静态状态数组 | **7** | `s_relisten_quarantine` / `_settle_start` / `_settle_dead` / `_wait` / `_next` / `_dead` / `_mark` | +| 常量 | **8** | `NET_RELISTEN_{QUARANTINE,GRACEFUL,SETTLE}_MS` / `NET_RELISTEN_SETTLE_{QUARANTINE,GRACEFUL}` / `NET_RELISTEN_{RETRY,DEADLINE}_MS` / `NET_LISTENER_PUMP_PERIOD_MS` | +| **搬移量小计** | **≈470 行** | 函数块 414 + 常量/状态注释块 55 | + +**跨边界引用点**(这个数字决定接口面大小,是本设计的核心依据): + +| 引用 | 处数 | 位置 | +|---|---|---| +| `s_relisten_quarantine[fd] = ` 外部**写** | **2** | `handle_disconnect_event`(GRACEFUL)、`handle_timeout_event`(QUARANTINE) | +| `net_relisten_try()` 外部调用 | **4** | `NET_MSG_RELISTEN` 处理、`net_recv_locked`、`net_accept_locked`、`net_pending_tick` | +| `net_relisten_clear()` 外部调用 | **2** | `net_socket`(新 fd)、`net_close_locked` | +| 自愈 tick 调用 | **2** | `net_process_messages`(`retry_tick` + `pump`) | + +> **关键观察**:状态数组的**外部写只有 2 处**、且都是「登记沉降等级」这一个语义。 +> 这说明模块边界天然干净——**接口面只需 4 个函数**,不需要暴露任何数组。 + +### 10.3 边界划分 + +**搬入新模块** `Drivers/BSP/NET/net_listener.c`(**真机专属**,sim 不编): + +| 搬入 | 理由 | +|---|---| +| `net_relisten_locked` / `net_relisten_try` / `net_relisten_retry_tick` / `net_relisten_clear` / `net_relisten_done` | 「维持 fd 处于 LISTEN 态」的全部机制 | +| `net_listener_pump` | 唯一的周期性重建决策者(P2a 成果) | +| `net_drain_chip_rx` | 重建前的排空(只被 relisten 调用) | +| 7 个 `s_relisten_*` + 8 个 `NET_RELISTEN_*` / `PUMP_PERIOD` 常量 | 状态与策略私有化 | + +**留在 `net_socket.c`**(并说明理由,避免"看着像就搬"): + +| 留下 | 理由 | +|---|---| +| `net_listen_locked` | 是**应用 API 路径**(首次监听,含 §9 待议的忙等 P2b),且要处理 BOUND/CREATED 状态门与缓冲——属"连接层建监听",不是"维持监听" | +| `net_accept_locked` | 产出的是**连接**(LISTENING→ESTABLISHED 迁移),属连接层;它只**调用**新模块 | +| `net_lost_connect_probe` | 做的是"补 accept"(连接建立),不是重建监听 | +| `net_listener_armed` | **双端公开 API**(sim 也实现);纯 RAM 查询,很薄,留在原处避免 sim 缺符号 | +| `net_resync_listen` | 公开 API + 消息分派;它**只改本层状态**,本就不调 relisten | +| `net_reconcile_tick` | ①② 是**连接**失同步自愈(③④ 已在 P2a 移交 pump) | +| `net_sock_t` 全部字段 | 数据归 `net_types.h`(双端共享核心),**不随模块走** | + +> ⚠ `net_sock_t` 的 `last_peer_port` / `lost_conn_seen` 是**跨层字段**:新模块的 +> `net_relisten_locked` 要写它们(转登幽灵甄别基准),`net_lost_connect_probe` 与对账② +> 也要读写。**这是本次重构无法消除的残余耦合**(单连接模型的固有结果), +> 明确登记而非假装不存在。 + +### 10.4 接口设计(4 函数 + 1 枚举,全部窄接口) + +```c +/* net_listener.h —— 只被 net_socket.c include(内部头,不进公开 net_socket.h,故 sim 无依赖) */ + +/* 沉降/隔离等级(替代外部直接写 s_relisten_quarantine) */ +typedef enum { + NET_LISTENER_SETTLE_GRACEFUL = 2, /* 正常释放(DISCONNECT):等芯片收尾 100ms */ + NET_LISTENER_SETTLE_QUARANTINE = 1, /* 异常释放(TIMEOUT/RST):等在途落尽 + 排空残渣 400ms */ +} net_listener_settle_t; + +/* 登记「本 fd 的会话已结束,重建前需沉降」—— 由 handle_disconnect/timeout_event 调用 */ +void net_listener_settle(int sockfd, net_listener_settle_t level); + +/* 尝试重建一次(不阻塞);已在重试队列则返回 -1 —— 替代 4 处 net_relisten_try */ +int net_listener_try(net_sock_t *p_sock); + +/* 清本 fd 的重建/沉降状态(新 fd 或 close 时)—— 替代 2 处 net_relisten_clear */ +void net_listener_clear(int sockfd); + +/* 自愈节拍:D10 重试轮询 + 监听器 pump(合二为一,调用方只记一个入口) */ +void net_listener_tick(void); +``` + +**反向依赖(新模块需要 `net_socket.c` 提供的)**: +- `net_sock_t` 定义 + 字段(`net_types.h`,共享头,**不算反向依赖**) +- `s_net_socks` 数组 —— **问题点**:它是 `net_socket.c` 的 static。 + **解法**:新模块**不反推 fd**,一律用 `p_sock->ch395_sock`(值等于 fd,`net_socket` 初始化时保证) + 或调用方传入的 `sockfd`。这样**完全不需要访问 `s_net_socks`**,依赖面进一步收窄。 +- `net_set_errno` / `NET_ERR_RETURN_1`(错误码) +- `ch395f_*`(驱动,本来就是外部依赖) +- `HAL_GetTick` / `lat_probe_*`(TEST_LATENCY_PROBE 构建) + +### 10.5 构建与注册 + +| 项 | 动作 | +|---|---| +| `MDK-ARM/STM32F407-Demo.uvprojx` | 加 `` 条目;`Drivers/BSP/NET` 已在 IncludePath(同目录,**无需新增路径**) | +| `simulator/CMakeLists.txt` | **不动** —— 新模块真机专属;sim 用 `sim/net_socket_sim.c` 自洽(无孤儿态、无 relisten) | +| 注册校验 | 跑 `registration-check`(双端注册反向校验)确认没有单边注册 | + +> ⚠ 按项目约定,新 `.c` 文件的注册走 **`new-source-file` skill**(uvprojx + CMakeLists 双端), +> 不要手工编辑工程文件。 + +### 10.6 验证门(执行时必须逐条过) + +1. **纯搬移可证**:搬移前后 `@build` 的 `Code`/`RO-data`/`RW-data`/`ZI-data` **完全不变** + (只挪位置、不加逻辑;任何数字变化都说明搬错了)。 +2. `@test` PASS + 回读 `No difference found`。 +3. 全量回归矩阵:`:21` 0ms×50 / 100ms×50、`:8888` 0ms×20、`:7003` 300ms×10、 + `ftp_regression` 11/11、Trap23 abort/graceful 各 5/5。 +4. 设备侧 `orphan listener` 增量 **0**(P2a 已达成的基线,不得回退)。 +5. `net_listener.c` **不 include** `net_socket.c` 的任何符号(除 `net_types.h`)—— + 用 `grep -n '#include' net_listener.c` 人工核对。 +6. 判据 §10.1 三条**逐条量化复核**(入口数 / 外部写 / 依赖方向)。 + +### 10.7 明确不做(边界外,避免范围失控) + +- **不动** `net_listen_locked` 的忙等(那是 P2b,已判暂缓)。 +- **不动** `net_sock_t` 字段布局(`last_peer_port` / `lost_conn_seen` 的跨层耦合本次保留)。 +- **不动** sim 后端(D6:双模型分裂暂不消除;P1 已让**应用层契约**对齐)。 +- **不做** P3 形态 A(listener 一等对象 + accept 返回虚拟句柄)—— 硬件不支持多监听,已出局(D1)。 +- **不引入**第二套并行机制(沿用「同一能力只有一个实现」的收敛原则)。 + +### 10.8 执行顺序建议 + +1. **先搬状态与常量**(7 数组 + 8 常量)→ 编译验证(此时功能等价,只换位置); +2. **再搬 7 个函数**,同步把 8 处外部引用改成 4 个窄接口; +3. **最后合并 tick**(`net_listener_tick` 内部调 retry + pump),`net_process_messages` 少一个调用点; +4. 每步独立提交、独立 `@build`;**第 3 步后**才跑完整台架矩阵(前两步是纯组织变更,可只跑 `@build` + `@test`)。 + +### 10.9 执行结果(2026-10-12,提交 `c1ab902`) + +**执行顺序调整(与 §10.8 的差异,据实记录)**:§10.8 的「先搬状态与常量」**单独做不可行**—— +状态数组被 `net_relisten_*` 函数直接读写,只搬状态不搬函数会**编译失败**。 +故第 1 步改为「**状态 + 常量 + 7 函数一次性纯搬移**」,第 3 步(合并 tick)**未做** +(收益仅少一个调用点,却会破坏「尺寸不变」这条硬判据,性价比不足)。 + +| 判据 | 结果 | +|---|---| +| `net_socket.c` 行数 | 4126 → **3656**(-471) | +| 新增模块 | `net_listener.c` / `net_listener.h`(真机专属) | +| **RW-data** | 980 → **980**(不变 ✓) | +| **ZI-data** | 143060 → **143060**(不变 ✓) | +| Code / RO-data | 197236 → 197228(-8)/ 17012 → 17000(-12) | +| 接口 | 8 处散点直调 + 2 处直接写数组 → **4 个窄接口** | +| `s_net_socks` 依赖 | **已消除**(改用 `net_get_sock` + `ch395_sock`) | +| 静态门禁 | 全绿(cppcheck / C 规范 / 双端注册 / 主机侧单测 12 套 / mem-numbers) | +| `@test` + 回读 | PASS + `No difference found` | +| 全量回归 | `:21` 0ms×50 与 100ms×50 各 **50/50**;`:8888` **20/20**;`:7003` **10/10**;`ftp_regression` **11/11**;Trap23 abort/graceful 各 **5/5** | +| 设备侧 `orphan listener` | **0**(保持 P2a 基线) | + +> **关于「尺寸完全不变」判据的修订**:实际执行发现该判据**过严** —— 接口改造 +> (fd 改传参、`net_set_errno` 提升可见性、新增 `net_pending_active`/`net_listener_settle`) +> **必然**改变代码生成。故修订为:**RW-data / ZI-data 必须完全不变**(数据总量不变), +> **Code / RO-data 允许极小变化**(本次各 -8 / -12 字节,相对 471 行搬移可忽略)。 + +**执行中暴露的三处「非纯搬移」(必要,均已注释留痕)**: + +1. **`net_set_errno` 由 `static` 提升为模块间可见** —— 它是 `static`,新模块链接期 + 报 `L6218E: Undefined symbol`;`NET_ERR_RETURN_1` 宏随之迁到 `net_socket.h` + (它依赖前者,不能只移宏)。 +2. **新增 `net_pending_active(fd)`** —— `net_listener_retry_tick` 原有「本 fd 有在途 + 挂起 recv 则跳过」(L3 保险,防重开 Socket 摧毁在途连接)直接读 `s_net_pending[i].active`; + 新模块不能碰该 static 数组,故加薄访问器。**语义原样保留**。 +3. **`net_listener.c` 必须真机专属** —— 它依赖 `ch395f_*` SPI 驱动,而 sim 是 Winsock + 后端(无 `ch395f_*`、无孤儿态、无重监听概念)→ **无法**在 sim 编译。故: + 只注册 Keil 端;并在 `scripts/check_registration.py` 的 `SIM_EXEMPT` **显式点名** + (该脚本既定的豁免原则正是「显式清单 + 每行理由,不整目录一刀切」,本例遵此)。 + +**教训(供后续搬移复用)**: +- 搬移前先查**跨边界引用面**(本次发现「状态数组外部写仅 2 处」),它直接决定接口面与风险; +- 「纯搬移可证」判据要选**对改造不敏感的量**(RW/ZI 数据总量),否则会被接口改造的 + 正常开销误报; +- 改完文件**必须重新 `git add`** —— pre-commit 校验的是**暂存区快照**,漏 add 会让 + 「本地已修好」与「提交时仍报错」自相矛盾(本次踩到,白跑两轮)。 diff --git a/docs/子系统设计/网络配置说明.md b/docs/子系统设计/网络配置说明.md index e3f123fe..c25ce587 100644 --- a/docs/子系统设计/网络配置说明.md +++ b/docs/子系统设计/网络配置说明.md @@ -1,6 +1,6 @@ # 网口设置页配置说明 -> 版本:v2.2 日期:2026-10-10 +> 版本:v2.3 日期:2026-10-11 > 适用工程:STM32F4-Base(STM32F407ZGTx + CH395F 以太网控制器) > 范围:UI「网口设置」页可配置的网口参数(IP 地址 / 子网掩码 / 默认网关 / MAC 地址)—— > 参数与默认值、持久化、生效链路、页面操作与权限。 @@ -77,6 +77,8 @@ gw=192.168.1.1 mac=00:00:00:00:00:00 ``` +> 出厂模板同时含 `[ftp]` 段(`user` / `pwd_hash` / `write`,见 §3.4)——本块只展示 `[network]` 部分。 + - **加载校验(逐键容错)**:单个键值非法(越界/格式错)不影响其他键——非法键回工厂默认并留痕(`parse: [network] <键> value invalid`);文件缺失 → 自动生成本模板;文件 > 4KB = 损坏 → 工厂默认 + 重建模板(坏配置不带崩网络)。 - **保存(外科手术式)**:注释 / 未知段 / 未知键逐行保留,受管键按 RAM 副本原位改写、缺失键补写;写盘 = 临时文件 `/config.TMP` + `f_rename` 原子替换(掉电不损坏目标文件)。 - **外部变更自动重载**(P4 阶段接入):2s 轮询检测 (size, mtime) 变化、4s 沉降(连续 2 次轮询稳定)后重载 `[network]` 段并重建网络——FTP 上传编辑后的文件无需重启即生效。 @@ -107,6 +109,47 @@ mac=00:00:00:00:00:00 INI 格式两端一致,文件可互读;mtime 变更令牌:MCU = FAT 打包时间(`fdate<<16\|ftime`,分钟级粒度),模拟器 = 宿主文件 mtime(秒级)。 +### 3.4 `[ftp]` 段:FTP 鉴权(2026-10-11 新增,N-19) + +FTP 服务(lftpd,:21)自 N-19 起启用口令鉴权。配置与 `[network]` **同文件、同引擎**(`net_cfg_load()` 内统一注册,键表与工厂默认见 `Drivers/BSP/NETCFG/ftp_cfg.c/h`;鉴权状态机在 `Drivers/BSP/NET/lftpd/lftpd_auth.c/h`,纯函数、主机侧可测): + +```ini +[ftp] +user=admin +pwd_hash=d82494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892 +write=1 +``` + +| 键 | 类型 | 工厂默认 | 说明 | +|---|---|---|---| +| `user` | STR(≤15 字符) | `admin` | 期望用户名,**大小写不敏感** | +| `pwd_hash` | STR(64 位 hex) | `d82494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892`(即口令 `admin`) | **SHA-256(user‖password)** 摘要,hex 大小写不敏感 | +| `write` | UINT(0/1) | `1` | 1 = 允许 STOR/DELE;**0 = 写命令一律 `550` 拒**(只读卷) | + +**鉴权流程**: + +1. `USER`:不论用户名对错一律回 `331`(不泄露"用户是否存在";错用户名在 `PASS` 阶段仍走同耗时哈希路径拉平时间)。 +2. `PASS`:计算 SHA-256(期望用户名‖口令),与 `pwd_hash` **恒定时间比较** → 对回 `230`、错回 `530`。 +3. **未登录命令门**:登录成功前仅 `USER` / `PASS` / `QUIT` / `FEAT` / `SYST` / `NOOP` 放行,其余命令一律 `530`。 +4. **失败锁定**:连续 **5 次**失败 → **锁定 60 s**(窗内所有 `PASS` 直接 `530`、不再计数);登录成功清零计数;锁定状态**全局跨会话**、易失(重启清零)。 + +**改口令步骤**(无 UI 入口;下载 `/config.ini` 编辑后上传,或经串口/烧录工具改写): + +1. 生成新摘要 hex(对 `user + 新口令` 拼接串做 SHA-256;下例中 `user` / `pwd` 为占位,替换为字符串字面量,如 `('admin'+'新口令')`): + + ```powershell + python -c "import hashlib;print(hashlib.sha256((user+pwd).encode()).hexdigest())" + ``` + +2. 把结果写入 `[ftp]` 段的 `pwd_hash=`(hex 大小写随意); +3. 上传回设备——配置引擎 poll 沉降 **≈4 s** 后重载,**下次登录生效**(既有会话不受影响)。 + +**忘口令恢复路径**:口令**不可**从 `pwd_hash` 反推(这正是哈希的意义)——经**串口/烧录工具改写配置**,或**重烧固件**回落出厂默认,然后立即改口令。 + +**出厂默认口令告警**:`pwd_hash` 仍为出厂值时,每次启动打 INFO 锚点 `[FTP] using factory default password`(改了 `pwd_hash` 即消失)。 + +> **诚实边界**:FTP 协议**明文传输口令**——本方案挡的是**未授权访问**,不挡**网络嗅探**(CH395F 上实现 TLS 不现实)。`pwd_hash` 只防 `/config.ini` 泄露后的**离线口令还原**,盐 = 用户名(无每机随机盐,局域网威胁模型下够用)。 + --- ## 4. 生效链路 @@ -265,6 +308,7 @@ ENTER 进入选中地址行 → `↑/↓` 调活动字节(0..255 **环绕**) | 日期 | 变更 | |---|---| +| 2026-10-11 | v2.3:**新增 §3.4 `[ftp]` 段(N-19 FTP 鉴权)**——`user` / `pwd_hash`(SHA-256(user‖password))/ `write` 字段表、鉴权流程(USER 恒 331 / 恒定时间比较 / 未登录命令门 / 5 败锁 60 s)、改口令步骤与忘口令恢复路径、出厂默认口令启动告警、明文嗅探边界 | | 2026-10-10 | v2.2:**删除旧档 NCFG 迁移**——`net_cfg_legacy.c/h` 整体移除、`cfg_store_legacy_path()` 撤销;旧档 `/cfg/netcfg.bin` 若存在则被忽略(不读、不删、不迁移),配置回落工厂默认。理由:该一次性路径把整条配置写链挂进启动期调用图,defaultTask 静态最坏栈链 +600 B,且本仓库无携带旧档的设备 | | 2026-09-14 | v2.1:存储迁移至**通用配置文件** `/config.ini`(INI `[network]` 段,cfg_file 引擎:逐键校验 / 外科手术式保存 / 原子写 / 旧档首启迁移 / 沉降轮询);`net_cfg_store.*` 弃用(由 `cfg_file_store.*` 取代);对外 API 不变 | | 2026-09-10 | MAC 显示改为常识格式 **`A4:94:B6:12:34:56`**(冒号分隔,17 字符);页面布局左移(名 x=4 / 值 x=54)以容纳 102px 宽度,编辑光标段偏移随之为 3i | diff --git a/docs/子系统设计/配置文件设计说明.md b/docs/子系统设计/配置文件设计说明.md index 291d1f92..09a59a99 100644 --- a/docs/子系统设计/配置文件设计说明.md +++ b/docs/子系统设计/配置文件设计说明.md @@ -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. 兼容与迁移 diff --git a/docs/待处理内容.md b/docs/待处理内容.md deleted file mode 100644 index bfdbda82..00000000 --- a/docs/待处理内容.md +++ /dev/null @@ -1,351 +0,0 @@ -# 待处理内容(统一待办登记簿) - -> **用途**:**全项目唯一的未闭环事项登记簿**——汇总"审计发现 / 台架观察 / 测试充分性与架构 -> 专项 / 任务排序"四类待办。原 `审查记录/项目审计改进清单.md`(P-x)、`审查记录/下一步任务规划.md` -> (阶段与基线)、`审查记录/测试与架构改进建议.md`(S1x / A-x)、`docs/待处理内容.md`(N-x / S-x / T-x) -> 于 2026-10-04 合并入本文件,**编号一律沿用,不回收**。 -> -> **只保留待办**:**已完成条目直接删除,不留变更记录**(历史见 `git log` 与各模块文档); -> 已闭环项的**结论与证据沉淀进对应模块文档/问题记录**,本文件不再重复。 -> **消化即销项**:事项修复/关闭后,直接删除对应条目,并同步清理其它文档对本编号的引用。 -> -> **不在本文件范围**:`问题记录/错误码传播审计.md`(E-xx,审计报告,非待办池)、 -> `问题记录/项目审查与优化建议报告.md`(体系层评审报告)、`测试说明/实机测试实施指南.md`(执行流程)。 -> -> **创建**:2026-09-22(L3 pending 台架收尾);**合并重写**:2026-10-04。 - ---- - -## 0. 编号空间与来源 - -| 编号 | 含义 | 原出处 | 备注 | -|---|---|---|---| -| `P0-x` / `P1-x` / `P2-x` / `P3-x` | 审计缺陷/改进项 | `项目审计改进清单.md`(2026-09-15/09-21 审计) | P0=功能 bug、P1=疑似 bug/健壮性、P2=改进、P3=卫生 | -| `N-x` | 跨文档观察/登记项 | `待处理内容.md` 登记簿 | 台架/审计发现、暂不在修复范围;**含硬件改版项**(需下一版硬件解决,固件侧只记口径,如 N-18) | -| `S-x` | 存储/日志管线待办 | 同上 | 存储侧健壮性 | -| `T-x` | 测试/验证基础设施待办 | 同上 | 工具链、CI | -| `S1~S14` / `A-1~A-7` | 测试充分性与架构改进专项 | `测试与架构改进建议.md`(2026-10-02 复审) | **编号不随条目删除而回收** | -| `E-xx` | 错误码传播审计发现 | `错误码传播审计.md` | **不并入本文件**,交叉引用 | -| 阶段 `A/B/C/D` + `B0~B3`/`C1~C6`/`D2~D6` | 任务排序分组 | `下一步任务规划.md` | 见 §2 | - -**严重度定义**(沿用审计口径):P0 = 确认功能 bug|P1 = 疑似 bug 或健壮性风险|P2 = 改进|P3 = 卫生。 -**置信度**:high = 代码级确证|medium = 依赖外部条件(手册/硬件/产品定义)。 - ---- - -## 1. 未闭环一览(按严重度) - -| 级别 | 数量 | 关键项 | -|---|---|---| -| **P1** | 0 (+1 收尾) | P1-10 测试固件构建实测闭环(主体已闭环,见 §3.1) | -| 登记簿 | 9 | 网络:N-2(49s 日志静默,**低风险观察项**)、N-3(L5 基线重验)、N-4(pending 二期)、N-16(A2/A4 专项负向验证)、N-17(探针窗口心跳缺口);存储/日志:**S-3(USART1 TX-only 根治)**、S-4(dhara 重试自旋)、S-5(可选全序列卷锁);硬件:**N-18(8 路电气量分属两转换器 = V0.1 缺陷,V0.2 改版项)** | -| 测试/基础设施 | 5 | T-7(CI runner 未就绪)+ 测试架构专项 **S12**(断言宏)、**S1-c**(可选单测)、**A-4**(采集链路测试约束)、**A-5 残余**(CI 体积门禁盲区) | - -> **网络侧另有 E-xx 未闭环项**(E-12/15/16/23/24/25/26/27/28/30/47/48/49/50/58/59/61,共 17 项) -> 与 1 项回归监督(E-62),详见 `问题记录/错误码传播审计.md` §3。本文件不重复登记。 - -### 1.1 结构性判断(排序依据) - -1. **网络层已是全项目最成熟子系统**(pending 机制 + 四份 Trap 记录 + 量化基线 + 探针族)。 - 剩余网络工作都是"二期扩展/收口",**没有紧急项**。 -2. **产品功能缺口正在收口:ADC 采集链路(B1)已闭环,RS-485(B2)仍为空桩**。 - `adc_task` 已由空桩转为真实消费者并 `@test` 实机闭环(2026-10-10:`frame_seq` 严格 1000 Hz - 递增、`idle=0`、`frst_err=0`);`rs485_task` 仍为空桩,且受 E-59 门禁挡住(E-50 / E-49 未修)。 - DTU 的核心职责(数据采集与转发)中,**采集侧还差 RS-485 一路**——即 E-59 那条"休眠缺陷闸门"的另一面。 -3. **验证债已基本还清,重心是"功能债"**:剩余验证债仅 **N-3(L5 基线重锁,待 PC peer)** - (原 **S-1 栈风险已于 2026-10-10 削链闭环并销项**:defaultTask 静态最坏链 5312 → 2776 B、 - 实测峰值 3392 → 2152 B,**未抬栈**,见 `docs/内存管理/RAM使用分析与优化方案.md`)。 - **当前最值得投入的是阶段 B(ADC/RS485 功能)**。 - ---- - -## 2. 任务排序(阶段 A/B/C/D) - -> 排序原则:**先降风险(产品级故障)→ 再补功能(核心链路)→ 再清结构债 → 最后卫生**。 -> 本节只给「做什么 / 为什么现在 / 入口在哪 / 完成判据」,明细指向 §3 对应条目。 - -### 2.1 阶段 B:核心功能补齐(产品价值最高,产品定义已确认) - -> 目标:让 DTU 真正采到数——这是当前**最大的功能空洞**,**当前最高优先级**。 -> **B0 产品定义已于 2026-10-10 确认并销项**:上行 **IEC 60870-5-104**(以太网 TCP 2404)/ -> 持续采集 / **1000 Hz(20 点/周波)** / RS-485 **对下 ModBus RTU 采集**;结论详见 -> `docs/算法/继电保护功能复现规划.md` §1.5,并已同步 `docs/产品需求/产品需求设计文档.md` §5.9/§5.10/§9。 -> **B3(纪律门禁)已于 2026-10-10 固化进 `AGENTS.md` 并销项**;同日 TPAFE 侧前置缺陷 -> **E-44 / E-45 / E-58 全部修复** → **B1(ADC 采集链路)已于 2026-10-10 实现并 `@test` -> 实机闭环销项**(`adc_task` 真实消费者 + 建任务 + 栈预算 + 台架 1000 Hz 确认); -> B2 仍欠 **E-50 / E-49**(RS485 驱动契约未修,门禁仍挡着)。 - -| 序 | 任务 | 前置 | 说明 | -|---|---|---|---| -| **B2** | **RS-485 采集链路实现**(消化 E-50、E-49) | B0 ✅ + **E-59 门禁**(**尚未满足**:E-50 / E-49 未修) | 双缓冲 + 序号(防包撕裂)+ DE 收尾(超时路径等 TC)+ 协议状态机(`App/protocol/` 正是为此预留);**B0 定用途 = 对下 ModBus RTU 采集** | - -**建议**:B1 已闭环;下一步是 **B2**——但 B2 之前必须先修 **E-50 / E-49**(RS485 驱动契约未修,E-59 门禁仍挡着)。 - -### 2.2 阶段 C:结构债清理(二期)+ 存储侧健壮性 - -| 序 | 任务 | 说明 | 出处 | -|---|---|---|---| -| **C1** | **pending 二期(ACCEPT + SEND)** | 沿一期机制扩展;同期消化 E-25(net_select 语义)、E-24/E-27/E-28(跨上下文收口);基线重锁 + 探针族扩充 + 看门狗接 selftest。**设计基线见 §5.1** | 第二档 #2 / N-4 | -| **C3** | **存储侧日志管线**(消化 N-2) | 丢弃可观测性**代码已具备**(`console_log.c` `s_con_drop`+`[ERR] CONSOLE dropped`、`sys_log_mcu.c` 队列满 `DBG_ERROR`、TX 僵尸通道自愈);V5 30 min 日志**未复现 49s 静默**(运行期 >10s 静默全为 ~28s、均在 `e02_probe` 窗口内=N-17)。剩余:与 S8(非阻塞日志)合并评估 + **S-3(USART1 TX-only 结构根治)** | N-2 / S-3 | -| **C5** | 一档剩余 | E-49(RS485 DE 收尾)、E-15(ringbuf 定去留——**已确认零调用者,倾向删除**)、E-61(SD2506 注释订正)、E-16(cfg_file 段名长度)、E-26(UDP 定案,倾向移除)、E-23、E-30 | 一档/二档 | -| **C6** | **USART1 TX-only 结构根治**(S-3) | 灭 ORE 僵尸机制根源:改 `.ioc` 令 USART1 不使能 RX / PA10 不复用为 `USART1_RX` → CubeMX 重生成 → 跑 `check_cubemx_survival.py`。属结构性改动,需单开一轮 + 复跑台架闭环 | S-3 | - -### 2.3 阶段 D:验证与卫生(常态化) - -| 序 | 任务 | 说明 | -|---|---|---| -| **D2** | N-3 L5 基线重锁 | 下次 Phase 13 窗口顺带做(低风险:阈值余量 >8×;**需 PC peer**) | -| **D5** | **N-16 专项负向台架窗口** | A2/A4 负向场景:PHY 拔线(需断 DTU 侧/关受管交换机端口)、A2 芯片级 `net_init` 失败(SPI 故障注入)、A4 短写压测 | -| **D6** | **N-17 探针窗口心跳缺口** | 定位 `time_sync` 监听重建为何阻塞 NET/FTP/UI(~1s 缺口,余量仅 ~0.7s);评估缩短重建重试或让掉线路径不阻塞其它任务 | - -### 2.4 建议执行顺序(一图) - -``` -【阶段 B 功能】 B0 产品定义确认 ✅ ─┬─ B3 E-59 门禁固化(并行) -(当前最高优先) └─ E-58 原子快照 → B1 ADC ✅ → B2 RS485 - ↓ -【阶段 C 结构】 C1 pending 二期 ─ C3 日志管线(N-2/S-3) - ─ C5 收口 ─ C6 USART1 TX-only - ↓ -【阶段 D 常态】 D2 N-3 / D5 N-16 负向 / D6 N-17 探针窗口 -``` - ---- - -## 3. 待办详述 - -### 3.1 P1(1 项收尾:P1-10 剩构建实测闭环) - -#### P1-10 测试固件静态 RAM 预算(基线已重建、测试缓冲已瘦身,剩构建实测闭环) -- **位置**:`MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct`(`RW_IRAM1 0x20000000 0x00020000` = 128KB;芯片 192KB 的上部 64KB 有意未映射,克隆芯片兼容性考量) -- **历史**(2026-09-15 审计 / 09-16 复现):产品构建静态镜像曾 = 123.6KB/128KB(余量仅 4.4KB);HW 全阶段固件的 PHASE5 三缓冲(3×4093B)→ 超 8.7KB(`L6406E` 实测);NET 固件的 32KB `s_rx_buf` → 超约 28KB。 -- **进展**(2026-09-21,已复核):① 预算基线重建(`docs/内存管理/RAM使用分析与优化方案.md` v1.2):FreeRTOS 堆迁 CCM,任务栈改由 CCM 堆分配;产品构建 RW_IRAM1 余 ~49KB;② NET 层测试 32KB `s_rx_buf`/`s_tx_buf` 已移除,改小局部缓冲;③ STORAGE 套件固件 2026-09-20/21 实测可构建可运行。 -- **剩余**:① CH395F 套件(`TEST_SUITE_CH395F`,HW 全阶段 / NET 层 7 配置)实际构建确认链接通过——PHASE5 三静态缓冲 3×4093B 仍在,理论余量足够但未实测;② `@test` 闭环;③ **不建议**扩 scatter 到 192KB(破坏既有克隆芯片兼容决策)。 -- **置信度**:high(基线数据来自 2026-09-21 map;测试固件构建未复验)。 - -### 3.2 登记簿(N-x / S-x / T-x) - -**网络(CH395F / net 层)** - -| # | 事项 | 发现 | 证据与现状 | 出处 | -|---|---|---|---|---| -| N-2 | **49s 全系统日志静默(defaultTask/NAND syslog 排空停滞)**——串口日志 49s 无行,期间 net 栈照常处理外部连接 → 日志管线停滞,队列满后各任务日志行被丢;系统功能不受损 | 2026-09-22(L3 台架 F2,gen 181) | **A/B 归因:既有存储侧现象**。**2026-10-03 降级为低风险观察项**:① 丢弃可观测性**代码已具备**(`console_log.c` `s_con_drop` + `[ERR] CONSOLE dropped`;`sys_log_mcu.c` 队列满 `DBG_ERROR`;UART TX 僵尸通道自愈);② V5 30 min **未复现 49s**(>10s 静默全为 ~28s、均在 `e02_probe` 窗口内=N-17)。**残余待办**:与 S8(非阻塞日志)合并评估 | `错误码传播审计.md` E-02 阶段二 | -| N-3 | **L5 队列往返基线(1223/988µs)的 L3 后重验**——L5 测 DONTWAIT 队列全程;L3 对该路径逐位不变 → 机制上无影响,但基线数值属分片时代,严格重锁需跑一轮 Phase 13(需 PC peer) | 2026-09-22(L3 收尾留档) | 低风险:10ms/100ms 阈值余量 >8×;下次 Phase 13 窗口顺带重锁即可 | `链路延迟测试说明.md` §6.4 L5 注¹ | -| N-4 | **pending 机制二期:ACCEPT + SEND 阻塞路径 pending 化**——E-06、E-20、E-21 剩余分量共用该机制扩展;届时 L3/L5 基线再重锁、探针族扩充、看门狗接 selftest 留痕、模拟器差距清单 +1 | — | 一期(RECV)已落地并入主线;架构现状见 `Net层阻塞模型架构设计.md`,二期设计基线见 §5.1 | `错误码传播审计.md` 第二档 #2 | -| N-16 | **A2/A4 专项场景台架验证待办**(正常路径已实测零回归):① PHY 拔线负向测试——断链需断 **DTU 侧**线缆或关**受管交换机端口**;② A2 芯片级 `net_init` 失败分支(需 SPI 故障注入);③ A4 短写压测(需远显客户端 + UI 推流压力) | 2026-10-03(台架拓扑 = CH395↔交换机,拔线操作受限) | 待专项台架窗口 | `实机测试实施指南.md` §10 | -| N-17 | **`time_sync` 30s 挂起客户端掉线 + 监听重建期间,网络任务心跳缺口达 800ms~1s(声明 stall 但未复位)**——V5 长跑 44 次 `task stall: mask=0x00000001`,**100% 与 `e02_probe` 的 30s 挂起客户端触发 `time_sync: no TIME packet in 30s, drop conn` → 监听重建(约 4s)重合**;隔离实验(仅 FTP,120s)**0 次 stall** → 探针场景专属。缺口(~1s)< IWDG 最坏超时(1701ms),余量仅 ~0.7s。**待办**:定位监听重建为何阻塞 NET/FTP/UI;评估缩短重建重试(5×2s)或让掉线路径不阻塞其它任务 | 2026-10-03(V5 台架) | `实机测试实施指南.md` §4;`子系统设计/看门狗设计说明.md` | - -**存储 / 日志管线** - -| # | 事项 | 发现 | 证据与现状 | 出处 | -|---|---|---|---|---| -| S-3 | **USART1 TX-only 结构根治(灭 ORE 僵尸机制)**——ORE 锁存在外设软复位后毫秒级重锁(PA10 悬空 + RX 使能),是 2026-10-02 台架"背靠背 drain 拉长主循环 → 喂狗节奏饥饿 → IWDG 误复位"的**机制根源**。现仅有缓解(单次 drain 250ms 预算 + 发送故障后 300ms 冷却),**未根治** | 2026-10-02(台架 gen-288) | **待办**:改 `.ioc` 令 USART1 为 TX-only → CubeMX 重生成 → 跑 `scripts/check_cubemx_survival.py`。结构性改动,需单开一轮并复跑台架闭环 | `子系统设计/控制台单写者日志设计说明.md` §12(含 O4) | -| S-4 | **dhara 重试自旋残留**——`try_recover()` 的 `DHARA_MAX_RETRIES`(8) 自旋在坏块密集时仍可能长时间占用 defaultTask(喂狗点) | 2026-10-02 | 低优先:现有 `vol_lock` + 喂狗节奏缓解;若台架再出"存储侧喂狗饥饿"再排期 | `Lib/dhara/map.c` | -| S-5 | **可选全序列卷锁**——现卷锁仅覆盖 `syslog` / `cfg` / `lftpd` 三条路径(`vol_lock_wait(0)` try 语义),其它潜在访问点(如 UI 读日志)未纳入 | 2026-10-02 | 待评估:先确认是否真有第二写者;无则关闭本项 | `App/fs/vol_lock.{c,h}` | - -**硬件(V0.1 缺陷 → V0.2 改版项)** - -| # | 事项 | 发现 | 证据与现状 | 出处 | -|---|---|---|---|---| -| N-18 | **8 路电气量分属两片转换器(V0.1 硬件缺陷,需下一版硬件修改)**——UA/UN/IA 走 TPAFE5160(16 位、8 通道同步、±5 V 双极性),UB/UC/IB/IC/IN 走 STM32 ADC(12 位、顺序扫描、0~3.3 V 单极性):① 两片转换器无共同采样触发 + STM32 侧顺序扫描(9 通道一轮 ≈ 67 µs)→ 跨转换器通道**无同一时刻性、相位关系不确定**,主板侧功率/功率因数/相角/对称分量类运算不成立;② 同族电气量精度与量程不一致(LSB ≈ 152.6 µV vs ≈ 806 µV,且 STM32 侧前端无中点偏置、负半周削顶);③ 语义与硬件归属交叉,固件映射/UI 通道序/自检注入须逐通道查表绕开 | 2026-10-10 定性更正(**撤销** 2026-10-09 建档时"有意分配、不要修正"的说法) | **V0.2 改版要求**:8 路电气量 UA/UB/UC/UN/IA/IB/IC/IN **全部接 TPAFE5160**,与 TPAFE 上 5 路备用路(AIN5/13/7/15/12)对调串联电阻在转换器一侧的目的地——元件数量与料号不变,属走线级改动。**V0.1 现板软件口径**:主板侧只做逐通道独立监视,禁止跨转换器做相位/功率/功率因数/矢量类运算(此类量取子板计量模块结果)。固件侧消费者已落地(B1,2026-10-10,按语义映射表取数)→ V0.2 改版只改映射表内容,上层语义与代码不动 | `硬件平台/ADC通道映射说明.md` §1/§7;`硬件平台/V0.1_网表详细分析报告.md` §1.4 V10、§6-3 | - -**测试与验证基础设施** - -| # | 事项 | 优先级 | 问题与证据 | 处置方向 | 出处 | -|---|---|---|---|---|---| -| T-7 | **CI runner 未就绪(部署前置)** | 中 | `.gitea/workflows/ci.yml` 已落地,但 CI 仅在 runner 就绪后才真正生效:① Gitea 实例 `[actions] ENABLED = true`;② 注册 Windows runner(labels 含 `windows`),装 Python 3.x / cppcheck / pre-commit / MSVC | runner 未就绪前,**提交前仍须在本地跑 `scripts/ci_local.py` 或 `pre-commit run --all-files` 自查**(L0 唯一保障,不能因"已配 CI"而放宽) | `.gitea/workflows/ci.yml`;`测试说明/测试开发标准流程.md` §9 | - -### 3.3 测试与架构改进专项(S1x / A-x) - -> **编号约定**:**S1~S14 / T-1~T-5 / A-1~A-7** 是 2026-10-02 版复审的**条目编号**——编号**不随条目删除而回收**; -> 其他文档若引用这些编号,其证据见对应模块文档与 `git log`。已完成事项的证据沉淀: -> `测试说明/系统时钟单元测试说明.md`(S1-a)、`测试说明/配置文件单元测试说明.md`(S1-b)、 -> `测试说明/测试开发标准流程.md` §4.2.1(L0.5 适格台账)、`测试说明/模拟器差距清单.md`(S13)、 -> `测试说明/实机测试实施指南.md`(S4/S5/S7/S9/S11)、`问题记录/GD5F2GQ5UE_Trap_Records.md`(S9)。 - -**未闭环一览** - -| 编号 | 事项 | 状态 | 详述 | -|---|---|---|---| -| **S12** | 模块级轻量断言宏 | ⬜ 待做(第 4 波按需) | 3.3.1 | -| **S1-c** | `sys_log` / `time_sync` 主机侧单测 | ⬜ 可选(价值递减) | 3.3.2 | -| **A-4** | ADC / RS485 采集链路的**测试可验证性约束** | ⏸ ADC 侧随 B1 闭环;RS485 待 B2 | 3.3.3 | -| **A-5 残余** | CI 体积门禁盲区(`mem-numbers` 在 CI 被跳过) | ⬜ 未消除 | 3.3.4 | - -#### 3.3.1 S12:模块级轻量断言宏 ⬜ - -**动机**:`configASSERT` 只覆盖 FreeRTOS 内部;自研代码的"不该发生"靠散落的 `if`。 - -**设计要点**(**必须零生产开销**): - -```c -/* App/util/app_assert.h —— 示意,非最终实现 */ -#ifdef APP_ASSERT_ENABLE - #define APP_ASSERT(cond) do { if (!(cond)) { \ - DBG_ERROR("[ASSERT] %s:%d", __FILE__, __LINE__); vApplicationAssertFailed(__FILE__, __LINE__); } \ - } while (0) -#else - #define APP_ASSERT(cond) ((void)0) /* 生产构建:编译期消失,零符号零开销 */ -#endif -``` - -**⚠️ 关键纪律**: -- **不得**在中断 / 高频路径使用(`DBG_ERROR` 有代价); -- **不得**替代正常的错误返回(断言是"内部不变量",不是"外部输入校验"); -- 生产构建 `APP_ASSERT_ENABLE` **关闭**——与 `ENABLE_DESTRUCTIVE_TESTS` 同级策略。 - -**建议先在 1~2 处试点**(如 `net_socket.c` 的状态机不变量),验证收益后再推广。 - -**完成判据**:宏存在 + 生产构建 `@build` 0/0 且 `.map` 无新增符号;试点处有实际使用。 - -#### 3.3.2 S1-c(可选):`sys_log` / `time_sync` 主机侧单测 ⬜ - -**范围**:`App/log/sys_log_mcu.c`(定长 64B 记录编解码;落盘部分需桩)、 -`App/sync/time_sync.c`(协议帧编解码;与网络耦合,需先判定可解耦面)。 - -**为何低优先**:两者边界密度低于 `sys_clock` / `cfg_file`,且已有真机路径覆盖 -(`@test` 标记 / `time_sync_server.py`)。**按剩余收益评估后再决定是否做**, -范式完全复用 `看门狗单元测试说明.md`。 - -**完成判据**:`test/host/<模块>/run_unit_test.py` 退出码 0 + 挂 pre-commit 钩子。 - -#### 3.3.3 A-4:ADC / RS485 采集链路的测试可验证性约束 ⏸ - -**背景**:`adc_task.c` 已实现(B1,2026-10-10),`rs485_task.c` 仍是空桩,`App/protocol/` 只有 README 与 `.gitkeep`。 -本项不是"缺陷",而是**对上游 B2(B1 已完成)的设计约束**——应在动手前明确,否则后续无法补测。 - -**约束**(建议纳入 B1 / B2 设计): - -1. **协议解析必须与硬件 I/O 分离**——`App/protocol/` 的解析函数应接受 - `(const uint8_t *buf, size_t len)` 并返回解析结果,使其成为 L0.5 的**天然适格对象** - (参考 `cfg_file` 的"核心 + 后端"模式); -2. **采集缓冲必须双缓冲 + 序号**(上游 E-49 / E-50)——否则消费者读到撕裂数据, - 且**撕裂在单测里测不出来**(时序相关),必须在设计上消除; -3. **DE 收尾必须有超时路径覆盖**(RS485 半双工特有陷阱)。 - -**完成判据**:B2 设计文档体现上述三条;协议解析函数有对应 L0.5 单测。(B1 侧:ADC 无协议解析面,其"同一时刻性"由 E-58 原子快照保证、已闭环。) - -#### 3.3.4 A-5 残余:CI 体积门禁盲区 ⬜ - -**现状**:`mem-numbers` 钩子**在 CI 上被跳过**(CI 无 `.map`)→ **CI 不守体积门禁**。 -S14 已在 workflow 显式提示"体积门禁未执行"(`::notice::`)并在 `CI配置指南.md` 声明, -但**盲区本身未消除**——看到 CI 全绿仍可能误以为体积也守住了。 - -**可选处置**: -- 让 runner 具备 Keil 后,在 CI 增 `@build` job(成本高,需授权与许可证); -- 或把"体积校验"固化为**提交前必跑的本地门禁**(`scripts/ci_local.py`,当前已是事实标准)。 - -**完成判据**:存在**强制的**体积校验路径(CI 侧或本地侧),且文档明确其边界。 - -> **注**:本项与栈预算是同一话题的两面——**具体栈风险(原 S-1)已于 2026-10-10 削链闭环销项** -> (defaultTask 静态最坏链 5312 → 2776 B、实测 3392 → 2152 B,**未抬栈**),本项仍是**门禁缺口**。 - ---- - -## 4. 待确认(需手册/硬件/产品定义,暂不直接改码) - -1. TPAFE5160 是否有自动扫描模式(**已不影响**:1 kHz 硬件定时器触发已于 2026-10-10 由 **TIM6 更新中断**方案落地,不依赖自动扫描,见 `docs/子系统设计/ADC采样节拍设计说明.md`)。 -2. `APP_ADC_ENABLE` / `APP_RS485_ENABLE` 是否应在产品构建真实门控模块(当前宏已如实标注为"未接线占位",此处确认的是**是否接入真实门控**;B1 取数不依赖该宏)。 -3. RS-485 PD0 DE 的半双工方向语义需硬件实测确认(本次仅静态审查)。 - ---- - -## 5. 设计基线(供后续接入参考) - -### 5.1 pending 二期设计基线(ACCEPT + SEND) - -> **来源**:一期(RECV)架构现状见 `docs/网络/Net层阻塞模型架构设计.md`(含演进溯源附录); -> 本节仅保留**二期仍要用的设计基线**。 - -**一期已验证的机制骨架**(二期沿用,勿重新发明):独立静态 pending 表 + `net_pending_tick` -主循环节拍了结(五路:数据/到点/断连/同 fd 串行/重建取消必通知)+ 登记守卫(每 fd 至多一个、 -不覆盖、拒绝 netTask 自等待)+ 1s 看门狗强结算。 - -**二期扩展点**: - -| 项 | 目标 | 对应缺陷 | -|---|---|---| -| **ACCEPT pending** | accept 等待改为"登记 → CONNECT 事件完成",退役 100ms/200ms 轮询循环 | E-06(accept 轮询计时失真) | -| **SEND pending** | 发送续传改为"登记 → send_ready 完成",消除 1s/分片 stall 对队列的冻结 | E-02 残余(SEND 位置) | - -**二期门禁**(一期门禁的加强版): - -1. CH395F NET 层全套件(含一期未跑的 Phase 7 / Phase 10,视 RAM 预算); -2. L3/L5 基线重锁(一期已重锁 M3,二期须再锁); -3. `e02_probe.py` **探针族扩充**:close / reconfig / EOF / disconnect-during-pending 四专项; -4. `ftp_regression.py`(D20)+ E-06 假 425 场景**独立重跑**(一期是借 M3 压力档覆盖的); -5. UI 远端推流压力 + timeSync 三场景; -6. 产品 `@test` 闭环 + ST-LINK 全片回读; -7. `ram_report.py` / `stack_report.py` 前后对比(铁律); -8. 看门狗接 selftest 留痕 + 模拟器差距清单 +1(`net_socket_sim.c` 无法建模 pending)。 - -**已知代价(延续一期,需文档化)**:pending 完成判定随主循环节拍运行,被长操作占用时超时 -顺延(~1ms 级);二期把 SEND 也 pending 化后,该顺延面反而**收窄**。 - -**改动量估算**:再 ~300-500 行,集中在 `net_socket.c`;公开 API 不变。风险评级**中** -(执行器核心 + D 系列前科)——**大的是验证,不是代码**。 - -### 5.2 错误码通道四要素(CH395F 范式) - -> **来源**:`错误码传播审计.md`。GD5F 错误通道统一(E-10 / E-42 / E-53 / E-54 / E-56 / E-57) -> 即按此骨架落地(2026-10-03,套件回归 16/16 PASS)。**后续任何驱动 / 子系统建立错误通道时应沿用。** - -``` -① 分类:区分「超时 / 真故障 / 参数错」——不得混为一个返回码 -② 透传:底层错误码逐层上报,不吞不猜(禁 `(void)ret`) -③ 分级:可恢复(重试)/ 不可恢复(上报自检 + 降级)显式判定 -④ 留痕:不可恢复项写 syslog selftest,可恢复项计数并在 UI / 日志可见 -``` - -**关键纪律**(呼应 `AGENTS.md` 已有铁律):**自愈逻辑严禁把"名字 / 参数类错误码" -归入"卷损坏触发集"**——`.fschk` 无限重格式化事故即此类(2026-09-20)。 - ---- - -## 6. 诊断基线(供后续对照) - -> **2026-10-10 重采(栈)/ 2026-10-04 重采(堆)**:栈 = **运行时高水位 + armlink 静态最坏链双口径** -> (产品构建 `suite=NONE`,开机 + 空闲采样,串口 `[STACK]` + `stack_report.py`);静态区 = `.map` 受管快照。 -> ✅ **栈水位已回落**:2026-10-04 曾升到 defaultTask **3664 B**(登记 S-1);2026-10-10 削链 -> (删旧档迁移 + `FIL` 静态化 + 日志行缓冲按模块收紧 + 钩子脱离 HAL)后实测 **2152 B**、 -> 静态最坏链 **2776 B**,**未抬栈**即达标。完整核算见 `docs/内存管理/RAM使用分析与优化方案.md`。 - -| 项 | 当前值(栈:2026-10-10;其余:2026-10-04) | 出处 | -|---|---|---| -| 产品构建 Flash (ER_IROM1) | 266,632 B / 1024 KB(25.4%);ROM 合计 **267,372 B** | `.map` 受管快照 | -| 产品构建 RW_IRAM1 | **84,960 / 131,072 B(64.8%)** | `.map` 受管快照(`RAM使用分析与优化方案.md` §2.1) | -| 产品构建 RW_CCM | 49,184 / 65,280 B(75.3%) | 同上 | -| 实测堆 | free=8,664 / **min_ever=4,728 B** | V5 串口 `[STACK] heap` | -| **defaultTask 栈峰值** | **2152 / 4096(余 1944 B = 47%);静态最坏链 2776 B(余 32%)** | `[STACK]` + `stack_report.py`(产品构建) | -| netTask 栈峰值 | 920 / 3840(余 76%);静态 1872 B | 同上 | -| ftpTask 栈峰值 | 740 / 4096(余 81%);静态 1216 B | 同上 | -| uiTask 栈峰值 | 4016 / 16384(余 75%);静态 1336 B | 同上 | -| timeSyncTask 栈峰值 | 792 / 2048(余量 61%) | 同上 | -| adcTask 栈峰值(B1,2026-10-10 新建) | **496 / 2048**(余 1552 B / 75%,台架 `@test` 实测);静态最坏链 504 B | 台架 `@test` + `stack_report.py` | -| testTask 栈峰值(产品态空转) | 496 / 8192(余量 93%) | 同上 | -| 队列响应性(挂起客户端) | 92.5ms(pending 一期) | `e02_probe.py` | -| M3 压力档帧周期 | med 60.1 / p95 66.4 / max 69.3ms,零尖峰 | `链路延迟测试说明.md` | - ---- - -## 7. 内存预算(支撑 §6 与定栈规则) - -现行权威数据 = `docs/内存管理/RAM使用分析与优化方案.md` v1.2(2026-09-21,§2.1 受管快照): -任务栈在 CCM 堆(defaultTask 1024w / netTask 960w / ftpTask 1024w / testTask 2048w / uiTask 4096w / timeSyncTask 512w / **adcTask 512w**); -产品构建 RW_IRAM1 84,960/131,072 B(64.8%)+ RW_CCM 49,184/65,280 B(75.3%); -实测堆 `free=8,664 / min_ever=4,728` B;台账工具 `scripts/ram_report.py` + `scripts/stack_report.py`。 -**旧栈表/旧余量数字作废,勿引用。** - ---- - -## 8. 维护约定 - -- 本文件是**全项目唯一的未闭环事项登记簿**——"盘点 + 排序 + 明细"三合一,明细不再分散到多份文件。 -- **任一项完成后直接删除对应条目**(先销本文件条目,再清理其它文档对本编号的引用); - **不留变更记录**,历史见 `git log` 与各模块文档。 -- 已闭环条目的**结论与证据沉淀进模块文档**,本文件不再重复。 -- 编号一律**沿用且不回收**(P-x / N-x / S-x / T-x / S1x / A-x);`错误码传播审计.md` 的 E-xx 独立维护。 -- 阶段 B 完成后重写 §1(届时 ADC/RS485 应从未闭环消失)。 -- 新增发现按既有惯例分流:能立即修的进 §2(阶段 C5);需排期的进 §3 对应编号空间;跨版本观察的进 §3.2(登记簿)。 -- 上游引用关系:`问题记录/项目审查与优化建议报告.md`(体系层评审)、`问题记录/错误码传播审计.md`(E-xx)、 - `测试说明/实机测试实施指南.md`(执行流程)——三者均不在本文件合并范围。 diff --git a/docs/文档规范.md b/docs/文档规范.md index 5ef9e0f1..e74905d7 100644 --- a/docs/文档规范.md +++ b/docs/文档规范.md @@ -57,8 +57,10 @@ | 文件尾 | 必须以单个换行结束(`end-of-file-fixer`) | | 乱码 | 禁止出现 `U+FFFD`(替换字符,字节序列 `EF BF BD`) | -> ⚠️ **本仓库 BOM 铁律**(见 `AGENTS.md`):C 源码中 `Drivers/BSP/UI/*`、`App/task/ui_task.c`、`App/cfg/cfg_file.c`、`Lib/lv_conf.h` 等**带 BOM**,被工具改写后 BOM 会静默丢失 → ARMCC 改按 CP936 解码 → 中文字面量编译失败。 -> **文档(`.md`)不受此限**,一律用无 BOM UTF-8。但用脚本批量改写文档时,仍须**按原样回写换行符**(本仓库 `core.autocrlf=true` 且无 `.gitattributes`,工作区换行混杂),否则会产生整文件行尾漂移的巨型 diff。 +> ⚠️ **本仓库 BOM 铁律**(见 `AGENTS.md`「铁律:源码编辑与编码」):C 源码中 `Drivers/BSP/UI/*`、`App/task/ui_task.c`、`App/cfg/cfg_file.c`、`Lib/lv_conf.h` 等**带 BOM**,被工具改写后 BOM 会静默丢失 → ARMCC 改按 CP936 解码 → 中文字面量编译失败。 +> **文档(`.md`)不受此限**,一律用无 BOM UTF-8。 +> +> **行尾策略(2026-10-09 起写死进 `.gitattributes`,本节是权威口径)**:`* text=auto eol=lf`(仓库 blob 与工作区统一 LF)+ `*.bat`/`*.cmd` 强制 CRLF(cmd.exe 对 LF-only 的 goto/标签不可靠)+ PNG/TTF/DLL/LIB 等显式 `binary` + `*.jpg` 走 LFS 且 `-text`(该条必须排在 `*` 之后才能覆盖)。**不要再依赖 `core.autocrlf`,也不要把工作区文件"改回 CRLF"**——CubeMX 重生成写的就是 LF;此前每次重生成后 40+ 个第三方文件在 `git status` 里显示"已修改",根因就是行尾由每台机器各自的 autocrlf 决定。用脚本批量改写文档时仍须**按原样回写换行符**,否则会产生整文件行尾漂移的巨型 diff。 --- @@ -100,6 +102,69 @@ `check_doc_links.py` 扫描前会剥离行内代码 span,故写法示例不会误报;但**反引号内的仓库根相对路径**(`docs/xxx.md`)仍会被校验——那是本工程的引用约定形态。 +### 3.5 引用章节:紧邻写法 + 别名表,让门禁能校验 + +引用某文档的某小节,写成 **`` `docs/xxx/yyy.md` §N.M ``**(文档名与 `§` 紧邻,中间只允许空白/反引号/括号)。 + +| 写法 | 门禁 | +|---|---| +| `` `docs/子系统设计/看门狗设计说明.md` §10.4 `` | ✅ 硬门禁(失效拦提交) | +| `` `看门狗设计说明.md` §10.4 ``(裸文件名,全库唯一) | ✅ 硬门禁 | +| `看门狗设计说明 §10.4`(省略 `.md`,文件名全库唯一) | ✅ 硬门禁 | +| `链路延迟 §8.3`(文档名**前缀**,全库唯一) | ✅ 硬门禁 | +| 「内存方案 §1.4」——已登记别名 | ✅ 硬门禁 | +| 「报告 §4.2」——昵称歧义、未登记 `[示例]` | ⚠️ 判不了 → 进 `--section-todo` 清单 | +| 本文档内「见 §3.1」 | ✅ 硬门禁(本文档必须有该编号;确实指别篇却省了文档名的,机器判不了 → 跳过并进待办) | + +**两档严重性**:目标明确(路径 / 裸名 / 前缀 / 别名 / 同句 `§A / §B` 链)→ **错误**;按自引用解析 → 默认**告警**,但 **pre-commit 钩子带 `--sections-strict`,两档都拦提交**(2026-10-11 起:全库告警已清零,清零之后就该守住)。手工跑想只看告警:去掉该 flag。 + +**别名表 `scripts/section_aliases.json`**:昵称 → 文档路径。登记一次,正文一个字不用改,引用就纳入硬门禁。**只登记无歧义的昵称**(「报告」命中多篇 → 不登记,逼着写清楚)。 + +**覆盖率必须看着**:每次跑 `doc-links` 都打印一行 + +``` +章节引用:共 1576 处,机器可判定 843 处(53%);跳过 → 昵称式 68 / 变更记录 419 / 外部手册 187 / … +``` + +比例只许涨不许跌。清 backlog: + +```powershell +& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all --section-todo +``` + +它列出全部机器判不了的 `§`(能唯一猜到的还给「建议改写:`docs/…` §N」),改一处涨一处;猜不到的就是**必须写清楚文档名**的那些。 + +> **口径怎么调出来的**:全库 1578 处 `§`。宽松版报警 173 处(几乎全是昵称误判)→ 加文档名解析 111 → 加紧邻 + `§` 链 + 前缀匹配 + 别名表 + 「父级编号不存在就不是自引用」判别 → 可判定比例 **7% → 58%(914 处)**,`--section-todo` 待办 **83 → 0**;清完 backlog 后**两档都拦提交**。变更记录小节(422 处)与外部手册编号(218 处)刻意不校验:前者是「当时那个编号」的历史指称,后者归厂商文档。 +> +> **把待办清零靠的三类动作**(按性价比排序;引号内是**改前**写法 `[示例]`):① **手册引用写全名**——「手册 §9.2.1」一律改成 `docs/芯片手册/CH395F.md` §9.2.1(CH395F / GD5F / SD2506 / RM0090 摘录里都有编号标题,改完立刻纳入校验);② **昵称写全或登记别名**——「架构 §5.5」→ `UI_设计架构.md` §5.5、「报告 §6.5」→ 全路径、「V0.0 报告 §4.1」→ `V0.0 §4.1`(别名表登记 `V0.0`);③ **路径补 `docs/` 前缀**——`子系统设计/xxx.md` 这种「从 docs/ 起算」的写法机器解析不到。 +> +> **两条写作铁律**(机器只看**同一行**):`§` 必须与文档名**紧邻**(换行/跨句就判不了);「见/按/该 + 昵称」这类含糊指代,宁可多打 30 个字符写全路径。 +> +> **首轮清账揪出的真断链(41 处,已修)**——`[示例]` 下列编号是**修复前的错值**,故意原样保留作反例:`链路延迟测试说明.md` 10 处引 §8.1,而该文 §8 下只有 8.2/8.3/8.4(§8.1 早被删);`GD5F2GQ5UE_Test_Guide.md` 引 §4.8(BBT 实为 §3);`自动化测试流程.md` 引 `ST-LINK_CLI_复位使用说明.md` §10(该文只到 §7);`模拟器差距清单.md` 引 §219(实为 §4.1);`实机测试实施指南.md` 引 §4.4 白名单(白名单在 `看门狗设计说明.md` §4.3);`V0.1_网表详细分析报告.md` 表格里 12 处裸 `§3.4`/`§5.3` 其实指 V0.0 报告(补 `V0.0` 前缀 + 登记别名)。**教训:编号会随重构漂移,只有机器校验能发现。** + +### 3.6 改名 / 挪目录:用 `scripts/rename_ref.py`,不要手工扫 + +**固定动作**(改名、挪目录、合并文档一律走这条,别靠 `git grep` 手改): + +```powershell +# 1) 预览:看它打算改哪些文件的哪些行(默认 dry-run,不落盘) +& '.venv\Scripts\python.exe' scripts/rename_ref.py docs/旧/名.md docs/新/名.md + +# 2) 确认无误后落盘:git mv + 改写全库引用 + 自动跑 check_doc_links --all 复核 +& '.venv\Scripts\python.exe' scripts/rename_ref.py docs/旧/名.md docs/新/名.md --apply + +# 整目录搬迁(末尾带 / = 挪进该目录并保留文件名;旧/新都是目录 = 前缀改写) +& '.venv\Scripts\python.exe' scripts/rename_ref.py docs/旧目录/ docs/新目录/ --apply +``` + +它覆盖的引用形态:反引号根相对引用、Markdown 链接(根相对与文档相对都按原风格改写)、**图片链接的 `../` 层级换算**(§3.2 那条实证坑由它兜住)、`#include "x.h"` 裸文件名、`MDK-ARM/*.uvprojx` 的 ``、`simulator/CMakeLists.txt` 的 `${PROJECT_ROOT}/…` 与 `${VAR}/…`、以及 skill / 脚本 / 配置里的路径串。 + +**它管不到的三件事**(脚本会在结尾提示): + +1. **昵称式引用**(「内存方案 §1.4」)与正文里的裸文件名散文 → 落盘后 `git grep -n '<旧文件名>'` 扫一遍; +2. **生成物**(`docs/images/引用关系图.*`)不改写 → 重跑 `& 'tools\refgraph\gen_refgraph.bat'`; +3. 改的是 `.c/.h` 且**新增了目录** → 还要把目录加进 `uvprojx` 的 ``(→ `new-source-file` skill),随后 `@build` 验证。 + --- ## 4. 门禁:文档-代码一致性(D-7) @@ -108,7 +173,7 @@ | 层次 | 钩子 | 检查内容 | 触发条件 | |---|---|---|---| -| **存在性** | `doc-links` | `docs/**/*.md` 里的链接、反引号路径是否指向真实文件 | 改动任意 `.md` | +| **存在性** | `doc-links` | `docs/**/*.md` 里的链接、反引号路径是否指向真实文件;`§N.M` 章节引用是否指向真实小节(§3.5) | 改动任意 `.md` | | **数值** | `task-table` | `README.md`、`AGENTS.md` 的 FreeRTOS 任务表是否与 `Src/freertos.c` 一致 | 改动 `README.md` / `AGENTS.md` / `Src/freertos.c` | | **体积** | `mem-numbers` | 内存/Flash 数字是否与最新 `.map` 一致(受管区 + 陈旧散句数字) | 改动内存文档 / `App`·`Src`·`Inc` 源码 / 脚本 | | **体积** | `check-added-large-files` | 阻止 >1MB 大文件入库 | 任意文件 | @@ -119,6 +184,10 @@ # 全库文档链接存在性 & '.venv\Scripts\python.exe' scripts/check_doc_links.py --all +# 改名 / 挪目录(先预览再落盘,见 §3.6) +& '.venv\Scripts\python.exe' scripts/rename_ref.py docs/旧/名.md docs/新/名.md +& '.venv\Scripts\python.exe' scripts/rename_ref.py docs/旧/名.md docs/新/名.md --apply + # 任务表一致性(只检查) & '.venv\Scripts\python.exe' scripts/gen_task_table.py --check @@ -192,6 +261,8 @@ - 外部 URL(`http/https/mailto`)与纯锚点(`#...`) - `docs/芯片手册/` 等外部资料目录 +**章节引用(D 类)另有的跳过**:昵称式引用(「内存方案 §1.4」)、非紧邻写法、变更记录小节内的历史指称、外部手册编号(RM0090 等)、目标文档本身不用数字编号标题。 + > ⚠️ 状态词按**就近**(±40 字符)判定,不是整行——否则文件名本身含「不存在」「删除」等字样时会误豁免同行的其它真实引用(2026-09-25 实测修正)。 --- @@ -229,7 +300,7 @@ | 项目级铁律、构建/烧录/测试命令、平台约束 | `AGENTS.md`(AI 协作上下文,**精简优先**) | | 开发者入门、架构速览、目录导航 | `README.md` | | 详细设计、专项说明、测试指南 | `docs/` 下对应子目录 | -| 跨文档待办登记 | `docs/待处理内容.md`(统一登记簿) | +| 跨文档待办登记 | `docs/任务规划/README.md`(统一登记簿) | | 评审报告与专项审计 | `docs/问题记录/` | > **避免重复**:同一事实只在一处详述,其它位置用链接指向。典型如 FreeRTOS 任务表——`Src/freertos.c` 是唯一事实源,`README.md`/`AGENTS.md` 的表由脚本生成,不在别处再抄一份。 diff --git a/docs/架构设计/STM32F4-Base存储架构说明.md b/docs/架构设计/STM32F4-Base存储架构说明.md index 137f09c7..8c0971ec 100644 --- a/docs/架构设计/STM32F4-Base存储架构说明.md +++ b/docs/架构设计/STM32F4-Base存储架构说明.md @@ -117,7 +117,7 @@ HAL_Init → SystemClock_Config → MX_GPIO_Init → MX_USART1_UART_Init 2. READ_ID (9Fh) └─ 校验 MID=0xC8, DID=0x52 3. BBT 扫描 - └─ 读每块第一页 (page 0) spare byte 0(列地址 0x800,手册 §12.4 初始坏块标记位) + └─ 读每块第一页 (page 0) spare byte 0(列地址 0x800,`docs/芯片手册/GD5F2GQ5UExxG.md` §12.4 初始坏块标记位) └─ 非 0xFF 即出厂坏块,写入 s_bbt[] 4. SET_FEATURE (B0h=10h) — 使能内部 8bit ECC 5. SET_FEATURE (A0h=00h) — 解除全部块保护 @@ -186,7 +186,7 @@ DMA 消除了轮询模式下 SPI 状态寄存器查检的逐字节 CPU 开销, - `gd5f_bbt_scan()` — 在 `gd5f2gq5ue_init()` 阶段、ECC 使能前扫描构建 BBT;运行时不再提供公开重扫接口(见 GD5F2GQ5UE_Trap_Records.md) - `gd5f2gq5ue_print_bbt()` — 打印 BBT 摘要(调试用) -> **注意**:`gd5f_private_bbt_scan()` 选择在 ECC 使能前扫描。按手册 §12.6,ECC 使能后整段 spare 仍可读取(仅 0x840~0x87F 的 ECC 校验区禁止编程),因此读取 0x800(spare byte 0)即便 ECC 开启也安全;但扫描放在 ECC 禁用阶段是更稳妥的默认做法。运行时坏块只来源于真实擦除/编程失败,不要主动重新扫描。 +> **注意**:`gd5f_private_bbt_scan()` 选择在 ECC 使能前扫描。按`docs/芯片手册/GD5F2GQ5UExxG.md` §12.6,ECC 使能后整段 spare 仍可读取(仅 0x840~0x87F 的 ECC 校验区禁止编程),因此读取 0x800(spare byte 0)即便 ECC 开启也安全;但扫描放在 ECC 禁用阶段是更稳妥的默认做法。运行时坏块只来源于真实擦除/编程失败,不要主动重新扫描。 ### 4.7 页内寻址模型(行地址 / 列地址) @@ -197,7 +197,7 @@ NAND 访问是**二维寻址**,两个地址含义不同: | 行地址(页地址 / page address) | 第几页 | `gd5f_page_read(page)` | 把哪一页从存储阵列搬进芯片内部 cache 寄存器 | | 列地址(column address) | 该页内第几个字节 | `gd5f_read_from_cache(column, ...)`(发送 `0Bh` + 2 字节列地址) | 从 cache 里偏移 `column` 处开始吐数据 | -**一页的内部布局**(手册 §12.6 Table 12-8):一页 = 2KB 主数据 + 64B 备用区(spare) = 2112 字节,页内连续编址为 column 0 ~ 2111: +**一页的内部布局**(`docs/芯片手册/GD5F2GQ5UExxG.md` §12.6 Table 12-8):一页 = 2KB 主数据 + 64B 备用区(spare) = 2112 字节,页内连续编址为 column 0 ~ 2111: ``` page (2112B) = main(2048B) + spare(64B) @@ -209,7 +209,7 @@ NAND 访问是**二维寻址**,两个地址含义不同: - `column 0 ~ 2047` → 主数据区(main data) - `column 2048 ~ 2111` → 备用区(spare),其中 **2048(0x800)就是 spare 的第 0 字节** -**为什么坏块标记的列地址是 2048**:main 区占据了前 2048 字节,spare 区从 2048 起算,故 spare byte 0 落在列地址 2048(0x800)。驱动把 `GD5F_PAGE_SIZE`(=2048)作为 `column` 传给 `gd5f_read_from_cache()`(`gd5f2gq5ue.c:307-308` 将其拼成 2 字节列地址),芯片即从 spare[0] 开始回数据——这正是出厂坏块标记位(手册 §12.4 / Table 12-6:First spare area location = Byte 2048)。 +**为什么坏块标记的列地址是 2048**:main 区占据了前 2048 字节,spare 区从 2048 起算,故 spare byte 0 落在列地址 2048(0x800)。驱动把 `GD5F_PAGE_SIZE`(=2048)作为 `column` 传给 `gd5f_read_from_cache()`(`gd5f2gq5ue.c:307-308` 将其拼成 2 字节列地址),芯片即从 spare[0] 开始回数据——这正是出厂坏块标记位(`docs/芯片手册/GD5F2GQ5UExxG.md` §12.4 / Table 12-6:First spare area location = Byte 2048)。 > 一句话:**page 选页,column 选页内字节偏移;main 占前 2048 字节,spare 从 0x800 起算。** @@ -565,7 +565,16 @@ if (nand_ftl_format() == 0) { 7. **单写者规则** — 存储由 **defaultTask 独占**:挂载 / `f_mkfs` / 卷健康探针 / `net_cfg_load` / syslog 落盘 / 配置引擎全收敛于它。netTask / uiTask **不得**直接触碰配置引擎或 FatFS。启用存储测试套件(`TEST_SUITE_STORAGE`)时存储所有权整体移交 testTask,defaultTask 必须**完全跳过**存储 bring-up(`App/task/default_task.c` 中 `#if !defined(TEST_SUITE_STORAGE)` 包裹,2026-09-25 自 `Src/freertos.c` 迁出)——否则后台文件写与测试的 `nand_ftl_deinit` 掉电模拟并发,互相踩坏 FTL/FatFs 状态(实测:mkfs 半途冻结 + BBT 被误写)。 8. **内存布局与 DMA/CCM 约束** — FreeRTOS 堆(含全部任务栈)已迁至 **CCM `0x10000000`**(64KB,直连 M4 零等待),但 **DMA1/DMA2 物理不可达 CCM**。GD5F 驱动 `GD5F_DMA_THRESHOLD = 1024`:栈上的小缓冲(dhara meta 读 132B、`dhara_nand_is_free` 64B)一律走 **CPU 轮询**、绝不经 DMA;页级大块(2048)才走 DMA 且源/目的**全部是文件作用域静态缓冲**(落 SRAM)。新增任何 DMA 收发缓冲必须①文件作用域静态②大小≥阈值③确认落 SRAM。违反任一条 = 静默数据损坏(CCM 首轮扩容实验实证)。换料注意:克隆片可能缺 SRAM3(CCM),必须重测。 9. **LVGL 绘制缓冲越界护栏** — LVGL 9.3.0 对 I1(1bpp) 整帧渲染会在其算出的 `stride*h`(160×160/8=3200) 之后**多写 1 个字节**,紧邻 `g_ui_draw_buf` 之后的 `.bss`(如 `ui_menu_view` 的 `s_frame_box`)指针最低字节被打成 `0xFF` → 野指针 → HardFault(`PC=lv_obj_get_scroll_y`)。`ui_port.c` 的 `g_ui_draw_buf` 已预留 `UI_DRAW_BUF_GUARD=32` 字节护栏吸收该越界写(逻辑尺寸仍按 `UI_FB_SIZE` 上报,渲染数学不变)。**勿删护栏、勿把缓冲缩到与邻接 `.bss` 紧邻**。Lib/LVGL 为第三方不改。 -10. **FatFs 严格 8.3 短名** — 详见 AGENTS。补充:含冒号 `:` 的名字被 FatFs 先解析为**盘符前缀** → `f_open` 返回 `FR_INVALID_DRIVE`(11) 而非 `FR_INVALID_NAME`(6);两者都属合法拒绝(测试用例须同时接受)。 -11. **FA_OPEN_APPEND 陷阱 / 短写** — 详见 AGENTS。追加写必须 `FA_WRITE | FA_OPEN_APPEND`;`f_write` 簇分配失败返回 `FR_OK`+短写(无错误码),判失败必须校验写回字节数。 +10. **FatFs 严格 8.3 短名** — 本移植 `Lib/FatFs/ffconf.h` 为 `FF_USE_LFN=0`,**只支持严格 8.3 短名**:主名 ≤8 字符、扩展 ≤3 字符、**不得以点开头**(2026-09-20 台架实证,`.fschk` 无限重格式化事故根因)。`.fschk` 会被 `ff.c` 的 `create_short` 把「点之后」当扩展域解析而超限 → `f_open` 恒返 `FR_INVALID_NAME`(6);这是**纯 RAM 里的名字判定,不碰盘**。`ff.c` `create_short` 全部拒绝点的地方已加 DEBUG 打印:`[ERR][FATFS] SFN reject (原因) seg='名字片段' bad_ch=坏字符`——非法名字调用看开机日志即可定位。自愈类逻辑**严禁**把名字/参数类错误码(`FR_INVALID_NAME` 等)归入"卷损坏"触发集(整卷重格式化不可逆)。补充:含冒号 `:` 的名字被 FatFs 先解析为**盘符前缀** → `f_open` 返回 `FR_INVALID_DRIVE`(11) 而非 `FR_INVALID_NAME`(6);两者都属合法拒绝(测试用例须同时接受)。 +11. **FA_OPEN_APPEND 陷阱 / 短写** — `FA_OPEN_APPEND`(0x30) = `FA_OPEN_ALWAYS|FA_SEEKEND`,**不含 `FA_WRITE`(0x02)**——不带写位打开的文件 `f_write` 恒返回 `FR_DENIED`(7)(这是当年 `/log` 写失败的真根因);追加写必须写 `FA_WRITE | FA_OPEN_APPEND`(2026-09-20 台架实测,`Lib/FatFs` 移植版)。该移植的 `f_write` 在簇分配失败时返回 `FR_OK` + 短写(**没有错误码**)→ 判失败必须校验写回的字节数。标志位为**旧式编号**:`FA_READ`=0x01 / `FA_WRITE`=0x02 / `FA_CREATE_NEW`=0x04 / `FA_CREATE_ALWAYS`=0x08 / `FA_OPEN_ALWAYS`=0x10 / `FA_OPEN_APPEND`=0x30 / `FA_SEEKEND`=0x20(`ff.c` 内部定义),与上游 FatFs R0.14 不同,**勿照抄上游代码示例**。 12. **崩溃可观测性(勿屏蔽)** — 已开启 `configCHECK_FOR_STACK_OVERFLOW=2` + `configUSE_MALLOC_FAILED_HOOK` + HardFault 现场报告(`Src/stm32f4xx_it.c`:裸 UART 打印 PC/LR/xPSR + CFSR/HFSR/BFAR/MMFAR 后复位,对照 `.map` 定位符号)。defaultTask **1024** words、testTask **2048** words 是**实测下限,勿降**。开机日志出现 `[ERR] STACK OVERFLOW task=` / `[ERR] HARDFAULT ...` / `MALLOC FAILED` 是精确定位线索,禁止屏蔽或降级。 13. **BBT 恢复模式** — 运行期坏块会随 TC-STO-03 注入累积;当介质被**误标脏**(如 DMA 误打 CCM 栈导致随机块被写坏并持久化)时,打开 `test/board/storage_test.h` 的 `ENABLE_STORAGE_RECOVERY_TESTS` 编译**恢复构建**,testTask 会执行 `gd5f2gq5ue_bbt_rebuild()`(出厂扫描清除运行期误标)+ `nand_ftl_format()` 清账后复位。恢复后容量回到基线(即扣除 BBT 保留池后的正常容量,非出厂裸容量)。 + +### 10.2 FTP 服务(lftpd)鉴权语义(2026-10-11,N-19) + +FTP 根目录 = 卷根,lftpd 是卷的网络文件入口(`Drivers/BSP/NET/lftpd/`)。N-19 起启用口令鉴权——`[ftp]` 配置段、鉴权流程与改口令方法的**权威口径**在 `docs/子系统设计/网络配置说明.md` §3.4,此处只登记存储侧语义: + +- **未登录命令门** — 登录成功前仅 `USER`/`PASS`/`QUIT`/`FEAT`/`SYST`/`NOOP` 放行,其余命令(含 RETR/STOR/LIST/DELE)一律 `530`。修复前 `cmd_user`/`cmd_pass` 对任意口令无条件回 `230` = **匿名全读写整卷**(N-19 根因)。 +- **STOR/DELE 写门** — `[ftp] write=0` 时写命令一律 `550` 拒,卷对 FTP 侧只读(`/log`、`/config.ini` 仍可下载不可改写)。 +- **全局失败锁定** — 连续 5 次鉴权失败 → 锁定 60 s(跨会话全局、易失、重启清零);纯 lftpd 层状态,不触存储。 +- **默认口令告警锚点** — 出厂 `pwd_hash` 在用期间每次启动打 INFO `[FTP] using factory default password`。 diff --git a/docs/架构设计/项目启动顺序管理.md b/docs/架构设计/项目启动顺序管理.md index 6cb8b692..3d1237aa 100644 --- a/docs/架构设计/项目启动顺序管理.md +++ b/docs/架构设计/项目启动顺序管理.md @@ -237,6 +237,28 @@ if (sys_evtHandle != NULL) { --- -## 7. 总结 +## 7. 内核启动前(`osKernelStart` 之前)的编码模式 + +> 本节是根目录 `AGENTS.md`「平台约束」中这条铁律的完整细节迁入处(2026-10-11,AGENTS.md 瘦身,不再当事实存放地)。 + +**禁令:`osKernelStart` 之前禁止调用任何 FreeRTOS / CMSIS-RTOS 创建类 API**——`osMessageQueueNew` / `osMessageQueuePut` / `osMutexNew` / `osSemaphoreNew` 等在内核启动前调用会**直接杀死启动**。 + +**2026-09-20 启动失败实证根因**:`syslog_mcu_init` 在内核启动前调用 `osMessageQueueNew`——这就是 09/19~09/20 "每次必死" 的代码层根因(修复提交 `82411cf`)。机理(见 `App/log/sys_log_mcu.c` 中 `syslog_mcu_init` 的函数注释):`app_main_init()` 运行在 `osKernelInitialize()` **之前**(`Src/main.c` 顺序 = `app_main_init` → `osKernelInitialize` → `MX_FREERTOS_Init` → `osKernelStart`),创建类 API 会在这一窗口提前拉起内核,其后的任务创建落在未完成初始化的内核状态上执行 → 启动失败。 + +**内核启动前代码的标准模式三件套**(现成参照:`App/log/sys_log_mcu.c`): + +1. **纯寄存器 / HAL 轮询操作**:启动段初始化只走不依赖内核的裸驱动路径; +2. **无锁静态缓冲**:内核前产生的日志先暂存到 `s_preboot[8]`(`SYSLOG_QUEUE_MAX = 8`,纯 BSS 数组,不碰任何内核对象;RTOS 前单生产者、后单消费者,故免锁)。投递点 `syslog_post` 按内核状态(`osKernelGetState()`)自动分流:内核未运行 → 写 `s_preboot`;已运行 → 走真队列; +3. **内核启动后惰性创建真队列**:真队列推迟到**任务上下文首次使用时**才创建——见 `syslog_drain`(仅 defaultTask 调用,调度器已在运行,`s_queue == NULL` 时才 `osMessageQueueNew`)。 + +**推论**:凡在内核启动前执行的初始化函数里,**不得创建任何 CMSIS 对象**。落地效果:`syslog_mcu_init` 现为"内核启动前安全空操作"(直接 `return 0`),真实初始化全部后移到运行期。 + +> **与 §3.3 的边界(勿混读)**:§3.3 说"CMSIS-RTOS v2 允许在 `osKernelInitialize()` 之后、`osKernelStart()` 之前创建内核对象"——那是 **CubeMX 生成器**在 `MX_FREERTOS_Init` 里创建事件组 / 任务的专用窗口;**手写代码**(尤其 `app_main_init` 这条早于 `osKernelInitialize` 的链)不享受该窗口,一律按本节禁令执行。 + +**配套判读**:违反本禁令的后果是启动段死机(无输出 + LED 不闪,此时没有任务、没有看门狗保护),定位方法见 §6 第 8 条「启动段(`osKernelStart` 之前)死机的判读法」——开机日志的最后一行 = 卡住的那一步。 + +--- + +## 8. 总结 **不要靠"等多少毫秒"来赌依赖就绪,而要靠"对方声明自己就绪了"来驱动启动**——Event Flags 把隐式的时间耦合变成显式的事件依赖,启动顺序自然清晰、健壮、易扩展。 diff --git a/docs/架构设计/项目架构总览.md b/docs/架构设计/项目架构总览.md index 33a854f9..0798379f 100644 --- a/docs/架构设计/项目架构总览.md +++ b/docs/架构设计/项目架构总览.md @@ -187,10 +187,10 @@ flowchart TB style NT fill:#efe,stroke:#484 ``` -**单连接约束**:CH395F 配置为多 Socket + TCP Server 单连接(`set_fun_para(0x09)`),Socket 0 复用监听与数据通道;FTP / 对时 / 远程显示三个 TCP 服务**串行**占用连接,连接断开后自动回到 LISTEN。 +**单连接约束**:CH395F 配置为 8 Socket(索引寻址)+ TCP Server 单连接(`set_fun_para(0x08)`,bit3 SOCKET_CLOSE;bit0 保留必须为 0),Socket 0 复用监听与数据通道;FTP / 对时 / 远程显示三个 TCP 服务**串行**占用连接,连接断开后自动回到 LISTEN。 > **FTP 启动门(2026-09-21 修复)**:`ftpTask` 必须等 `NET_READY` **且** `STORAGE_READY`(两位均 `NoClear`)才 `lftpd_start`——此前就绪位被其它等待者消费,FTP **静默不监听**(详见 §2.2 与 `docs/测试说明/FTP_Test_Guide.md` §8.6)。 -> **单客户端实测注意**:同一时刻只服务 1 个会话,旧会话异常断开后约 30s 才恢复监听。测试脚本必须内置连接重试(`test/host/ftp_up_dl_test.py`、`test/host/ftp_speed_test.py` 已内置),且**不要用裸 TCP 工具探测 21 端口**(探测会占掉唯一会话,见该指南 §8.7)。 +> **单客户端实测注意**:同一时刻只服务 1 个会话,旧会话异常断开后约 30s 才恢复监听。测试脚本必须内置连接重试(`test/host/ftp_up_dl_test.py`、`test/host/ftp_speed_test.py` 已内置),且**不要用裸 TCP 工具探测 21 端口**(探测会占掉唯一会话,见 `docs/测试说明/FTP_Test_Guide.md` §8.7)。 > 2026-09-21 实测:登录/TYPE I/PASV/STOR/RETR/LIST/DELE/QUIT 全通,128KB 上传下载逐字节一致(约 49/60 KB/s,低于阶段 5 裸 socket 基线,待查)。 --- @@ -314,7 +314,7 @@ flowchart LR | 控制台单写者日志(USART1 调试流) | `docs/子系统设计/控制台单写者日志设计说明.md` | | RAM 预算与优化 | `docs/内存管理/RAM使用分析与优化方案.md` | | 栈/内存观测工具 | `scripts/stack_report.py`(Keil 静态调用图 × 串口 `[STACK]` 水位,判定取两者更坏者)· `scripts/ram_report.py`(.map 静态占用)—— 用法与基线见内存方案 §0.6 | -| FTP 功能与陷阱 | `docs/测试说明/FTP_Test_Guide.md`(启动门 §8.6 / 单客户端 §8.7)· 脚本 `test/host/ftp_up_dl_test.py`、`test/host/ftp_speed_test.py` | +| FTP 功能与陷阱 | `docs/测试说明/FTP_Test_Guide.md` §8.6(启动门)/ §8.7(单客户端)· 脚本 `test/host/ftp_up_dl_test.py`、`test/host/ftp_speed_test.py` | | UI 设计架构 / 内容页 / 菜单 | `docs/UI设计/UI_设计架构.md` · `内容页架构.md` · `菜单页面设计.md` | | UI 自动化测试 | `docs/UI设计/UI_自动化测试流程.md` | | 驱动已知陷阱 | `docs/问题记录/{CH395F,GD5F2GQ5UE,SD2506}_Trap_Records.md` | diff --git a/docs/测试说明/ADC输入层单元测试说明.md b/docs/测试说明/ADC输入层单元测试说明.md new file mode 100644 index 00000000..51b63404 --- /dev/null +++ b/docs/测试说明/ADC输入层单元测试说明.md @@ -0,0 +1,277 @@ +# ADC 输入层单元测试说明(主机侧) + +> **模块**:ADC Input Layer(L1 采集与预处理,`App/algo/adc_input.c`) +> **测试类型**:**主机侧纯逻辑单元测试**(L0.5 层)——在 PC 上离线验证语义通道映射、 +> STM32 ADC1 的 DMA 乒乓帧消费(含半区选择)、TPAFE 快照消费与取数守卫,零硬件、零烧录、 +> 毫秒级、不占 MCU 的 RAM。 +> **状态**:✅ 已落地(**101** 用例,`/W4` 0 告警,已挂 pre-commit 钩子) +> **日期**:2026-10-10 +> **关联文档**: +> [`../../AGENTS.md`](../../AGENTS.md)(平台约束 · CubeMX 生成区铁律)、 +> [`测试开发标准流程.md`](测试开发标准流程.md)(测试分层与门禁总览)、 +> [`配置文件单元测试说明.md`](配置文件单元测试说明.md)(同层"强符号桩"范本)、 +> [`相量层单元测试说明.md`](相量层单元测试说明.md)(同批落地的 L2 姊妹篇)、 +> [`../硬件平台/ADC通道映射说明.md`](../硬件平台/ADC通道映射说明.md)(**语义映射权威口径**)、 +> [`../算法/继电保护功能复现规划.md`](../算法/继电保护功能复现规划.md)(L1-3 / L1-4 设计依据)、 +> [`../问题记录/错误码传播审计.md`](../问题记录/错误码传播审计.md)(E-44 / E-58 快照语义) + +--- + +## 1. 概述 + +### 1.1 为什么单独做主机侧单测 + +`adc_input.c` 把两片转换器的原始码值按**语义**统一暴露,其正确性的核心是三者一致: + +| 半边 | 内容 | 可测性 | +|---|---|---| +| **纯逻辑**(被测) | ① 语义通道映射表(L1-4):`UA/UB/UC/UN/IA/IB/IC/IN/TEMP` → `(来源, 下标)`;② STM32 ADC1 帧消费(L1-3,v2.0 DMA 模型):**Rank 顺序 ↔ ADC1 通道号 ↔ 语义** + **乒乓半区选择**;③ 取数守卫与 poll 契约;④ 状态码与四类故障计数(`seq`/`gap`/`err`/`hw`) | 纯查表 + 状态机,与硬件无关 → **可在 PC 上位穷举** | +| **硬件副作用** | `HAL_ADC_Start_DMA()`(挂载)与被测代码自己实现的三个 HAL 弱回调 `HAL_ADC_Conv[Half]CpltCallback` / `HAL_ADC_ErrorCallback`;`tpafe5160_snapshot()`(临界区) | 挂载是**外部符号** → 主机侧给同名强符号桩;三个回调由**被测代码**定义 → 测试直接调用它们注入"DMA 完成" | + +板端套件(`test/board/`)**无法验证本层的核心判据**: + +- **通道映射**:板端**无信号源**(见 `docs/硬件平台/ADC通道映射说明.md` §8),无法注入已知电压去 + 验证"读到的是哪只脚"。而映射表一旦写错(本项目 V0.1 有 13/17 路被置换),读到的会是 + **另一只脚**或**恒为陈旧值**,且**不会有任何报错**——这正是本层最危险的失效模式。 +- **HAL 故障路径**:`HAL_ADC_Start_DMA()` 挂载失败、DMA 完成计数不前进(节拍源停摆)、 + 超拍丢帧、HAL 错误回调在真机上极难稳定复现,且**读错乒乓半区**这种缺陷在板端只会表现为 + "读数偶尔怪"、无法定位。 + +主机侧的做法:给**每个 Rank 注入互不相同的码值**(第 i 个 Rank 写成 `100+i`),把 +"Rank 顺序 ↔ ADC1 通道号 ↔ 语义"整条链路**逐通道钉死**;"DMA 完成"由测试直接调用被测代码 +实现的半传输/传输完成回调注入(`dma_beat()`),故障路径用可注入桩一次覆盖。 + +> ⚠ 本单测**只覆盖 L1-3 / L1-4**。L1-5(通道标定)本层不做,故取到的是**未标定原始码值**; +> L1-1 / L1-2(周期触发与 TPAFE 快照链路)由 `adc_task` + TPAFE 驱动承担,板端套件覆盖。 + +### 1.2 被测对象与桩(**不改生产代码**) + +`adc_input.c` 的全部平台依赖都是**外部符号**,故测试单元只需提供**同名强符号桩**, +**生产代码零改动**(无任何 `#ifdef` 测试钩子)。 + +``` +test/host/adc_input/adc_input_test.c + ├── #include "adc.h" → stub/adc.h (HAL_ADC_* 原型 + hadc1 + ADC_CHANNEL_0..15) + ├── #include "tpafe5160.h" → stub/tpafe5160.h (仅 tpafe5160_snapshot 原型) + └── #include "adc_input.c" ← 生产源文件(App/algo/),原样纳入 +``` + +> **`adc_input.h` 用真实头**:它只依赖 ``,不拖 HAL,故接口契约原汁。 +> **`adc.h` / `tpafe5160.h` 必须用桩**:真实 `Inc/adc.h` 经 `main.h` 拖入整套 HAL; +> 真实 `Drivers/BSP/TPAFE5160/tpafe5160.h` 拖入 GPIO 宏——主机侧无法编译。 +> **桩目录必须排在 `App/algo` 之前**(`include_dirs` 顺序敏感,见 `run_unit_test.py`)。 + +| 平台接口 | MCU 端 | 主机侧桩 | +|---|---|---| +| `hadc1` / `hadc_other` | `MX_ADC1_Init()` 配置的 ADC1 句柄 | 两个占位对象:被测代码只**比较指针**(三个 HAL 回调的 `hadc == &hadc1` 守卫),从不读字段;`hadc_other` 专门用来证伪守卫 | +| `HAL_ADC_Start_DMA` | 挂载循环 DMA,长度 2×9(此后每拍由硬件自走) | 记录 `s_start_dma_calls` / `s_dma_buf` / `s_dma_len`,返回 `s_start_dma_rc`(可注入 `HAL_ERROR`) | +| `HAL_ADC_ConvHalfCpltCallback` / `HAL_ADC_ConvCpltCallback` / `HAL_ADC_ErrorCallback` | **由被测代码实现**(覆盖 HAL 弱符号),ISR 上下文 | 测试用 `dma_beat(base)` 直接调用:按 `seq & 1` 决定往哪个半区填 `base+i`,再触发对应回调——与消费侧的奇偶推导**互相证伪** | +| `tpafe5160_snapshot` | 临界区原子快照最新一帧 | 返回 `s_snapshot_rc`(1/0/-1);`rc==1` 时从 `s_fake_tpafe[]` 拷贝;记录 `count` 与 `dst` | + +桩里 `ADC_CHANNEL_0..15` 的**宏值等于通道号**(F4 HAL 即如此),故 `(uint8_t)ADC_CHANNEL_14 == 14` +与真机一致——这是"映射判据"能成立的前提。 + +### 1.3 测试环境 + +| 依赖 | 版本 / 说明 | +|---|---| +| 编译器 | 本机 MSVC(VS2019/2022/2026 任一),由 `run_unit_test.py` 自动探测 | +| Python | 仓库 `.venv` 或任意 python3(**纯标准库**) | +| 硬件 | 无(不需要开发板 / 串口 / ST-LINK / 信号源) | + +工具链探测 / `INCLUDE`-`LIB` 拼接 / 编译运行 / 退出码判定全部委托通用 harness +[`../../test/host/common/unit_test_harness.py`](../../test/host/common/unit_test_harness.py), +本模块 `run_unit_test.py` 只**声明** `TestBuild`(源文件 / 桩目录 / 汇总行标记)。 + +### 1.4 运行方式 + +```powershell +# 仓库根目录(统一用 .venv) +& '.venv\Scripts\python.exe' test/host/adc_input/run_unit_test.py +``` + +退出码:`0`=PASS / `1`=测试失败 / `2`=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +### 1.5 回归接入 + +| 项 | 说明 | +|---|---| +| **触发范围** | `App/algo/adc_input.c` / `adc_input.h`,或 `test/host/adc_input/**`、`test/host/common/unit_test_harness.py` 变动 | +| **门禁层级** | **L0.5 层**(介于 L0 静态门禁与 L1 板端套件之间):主机侧、秒级、纯逻辑 | +| **pre-commit 钩子** | `adc-input-unit-test`(本地钩子,`.pre-commit-config.yaml` §7e) | +| **汇总行** | `=== ADC Input Tests: 70/70 PASSED (failed=0) ===` | +| **与板端测试的关系** | 互补:本单测保证**映射 / 契约 / 故障路径**正确;板端保证 **HAL 配置(Rank 顺序、采样时间、引脚)** 与真机一致 | + +--- + +## 2. 测试用例 + +> 命名 `TC-AI-NNN`(AI = Adc Input;主机侧单测自定前缀,与板端套件缩写表互不影响)。 +> 全部在 `adc_input_test.c` 内,主机侧运行。被测源文件被 `#include` 纳入同一编译单元, +> 故**静态函数(`map_check` / `stm32_take`)亦可直接调用**(白盒)。 + +### 2.1 初始化与复位(`adc_input_init`) + +| 用例 | 断言 | 验证的设计点 | +|---|---|---| +| **TC-AI-001** | 前置:注入 `HAL_ADC_Start` 失败 → poll 后失败计数 = 1 | 统计可见性(前置构造) | +| **TC-AI-002** | 前置:注入 TPAFE 有效帧 → poll 后 `UA` 可取数且值正确 | 取数通路(前置构造) | +| **TC-AI-003** | `adc_input_init()` 返回 0 | 映射表自检通过 | +| **TC-AI-004** | init 后 `adc_input_stm32_rc() == ADC_INPUT_RC_NOT_RUN` | 状态码复位 | +| **TC-AI-005** | init 后 `adc_input_stm32_errs() == 0` | 失败计数归零 | +| **TC-AI-006** | init 后 TPAFE / STM32 **两侧** `adc_input_raw()` 均返回 -2 | 有效标志被清(不会取到上一轮陈旧值) | + +### 2.2 语义映射表(`adc_input_desc`) + +| 用例 | 断言(语义 → 来源 / 下标) | 权威口径 | +|---|---|---| +| **TC-AI-010** | `UA` → TPAFE `buf[6]` | `docs/硬件平台/ADC通道映射说明.md` | +| **TC-AI-011** | `UB` → STM32 `CH14` | 同上 | +| **TC-AI-012** | `UC` → STM32 `CH3` | 同上 | +| **TC-AI-013** | `UN` → TPAFE `buf[5]` | 同上 | +| **TC-AI-014** | `IA` → TPAFE `buf[2]` | 同上 | +| **TC-AI-015** | `IB` → STM32 `CH6` | 同上 | +| **TC-AI-016** | `IC` → STM32 `CH1` | 同上 | +| **TC-AI-017** | `IN` → STM32 `CH2` | 同上 | +| **TC-AI-018** | `TEMP` → STM32 `CH5`(V_ZL2) | 同上 | +| **TC-AI-019** | `adc_input_desc(ADC_SEM_COUNT)` → `NULL` | 越界防御 | +| **TC-AI-01A** | `adc_input_desc((adc_semantic_t)-1)` → `NULL` | 无符号比较兜底 | + +### 2.3 映射表自检(`map_check`,静态) + +| 用例 | 断言 | 验证的设计点 | +|---|---|---| +| **TC-AI-020** | `map_check() == 0` | 真实映射表自洽:TPAFE 下标 < 8;每个 STM32 映射通道都出现在扫描 Rank 内 | + +> 反向路径(表不自洽 → -1)**结构上不可达**,见 §4.1。 + +### 2.4 STM32 ADC1 帧消费(`stm32_take`,静态;v2.0 DMA 模型) + +| 用例 | 断言 | 验证的设计点 | +|---|---|---| +| **TC-AI-021** | 有新帧 → 返回 0 | 正常路径 | +| **TC-AI-022** | `adc_input_stm32_rc() == RC_OK` | 状态码 | +| **TC-AI-023** | 成功不计入失败计数 | 统计语义 | +| **TC-AI-024** | `HAL_ADC_Start_DMA` 只在 `init()` 挂**一次** | 外设生命周期(每拍不重启) | +| **TC-AI-025** | DMA 完成计数 `seq` = 1 | 计数只由 HAL 回调推进 | +| **TC-AI-026** | ⭐ **Rank → 通道号映射**:`UB` = CH14 / rank9 = 108 | **本层核心判据**(板端无法验) | +| **TC-AI-027** | `UC` = CH3 / rank4 = 103 | 同上 | +| **TC-AI-028** | `IB` = CH6 / rank7 = 106 | 同上 | +| **TC-AI-029** | `IC` = CH1 / rank2 = 101 | 同上 | +| **TC-AI-02A** | `IN` = CH2 / rank3 = 102 | 同上 | +| **TC-AI-02B** | `TEMP` = CH5 / rank6 = 105 | 同上 | +| **TC-AI-02C** | 取帧后 `UA`(TPAFE)仍为 -2 | 两来源互相独立 | +| **TC-AI-080..085** | ⭐ **乒乓双缓冲**:往"正在被 DMA 写的另一半区"填 900/800 系干扰值,`stm32_take()` 仍必须读到**刚完成那一半**(108 / 208) | **撕裂帧防线**(读错半区 = 半新半旧混合帧) | +| **TC-AI-086..089** | 连投两帧不消费 → 取到最新帧(408)且 `gaps()` = 1、`errs()` = 0 | **丢帧与停摆分开计数**(消费者超拍 ≠ 节拍源停) | +| **TC-AI-090..09A** | `HAL_ADC_ErrorCallback(&hadc1)` ×2 → `hw_errs()` = 2;用**别的句柄**调三个回调 → `seq` / `hw_errs` 均不动 | 三个 HAL 弱回调的 `hadc == &hadc1` **句柄守卫** | +| **TC-AI-030..034** | `HAL_ADC_Start_DMA` 返回 `HAL_ERROR` → `init()` 返回 **-2**、状态码 `RC_START`、计数 +1、STM32 侧无有效数据 | 挂载失败(一次性,非每拍) | +| **TC-AI-035..039** | 无任何 DMA 完成 → `RC_STALL`、**单拍不计故障**、`seq` 不前进、STM32 侧无有效数据 | 无新帧路径 + 相位伪影不误报 | +| **TC-AI-03A..03E** | 连续两拍无新帧 → `errs()` = 1;第三拍 → 2;恢复拍成功且 `gaps()` 仍 0 | 停摆判据 = **连续 ≥2 拍**;跨次单调累计 | +| **TC-AI-070..078** | ⭐ **停摆拍不得沿用上一拍旧值**:先成功一拍让 STM32 侧进入有效态(`UB`=108),再注入无新帧拍 → `adc_input_raw()` 必须返 **-2**;恢复拍重新可取数 | **陈旧帧**(2026-10-10 真机实测发现的缺陷,见下注) | + +> ⚠ **为什么 TC-AI-034 照不出这条缺陷**:它是"`adc_input_init()` 后**立刻**失败",此时 +> `s_stm32_valid` 本来就是 0,断言 -2 恒成立。真正的缺陷路径是**成功过之后再失败**: +> 本层只有一份 `s_stm32_raw[]`,而有效标志此前**只在成功时置 1、失败时从不清零** → +> 消费侧照样取数、照样压入采样环(`adc_input_poll()` 返回值只由 TPAFE 决定, +> `s_frames_skip` 抓不到),直接污染 20 点 DFT 窗。 +> 修复 = 失败支路清 `s_stm32_valid`(`App/algo/adc_input.c`)。 +> **这组用例在轮询版与 DMA 版都保留**:触发条件从"轮询超时"变成"本拍无新帧", +> **同一条缺陷、同一个防线**。 +> **反向验证已做(v2.0)**:把停摆支路改回 `valid=1` → TC-AI-039 / 073 / 074 / 074B 四条立即 +> FAIL(89/93 时点);把半区奇偶取反 → 11 条 FAIL;`STALL_RUN_MAX` 改 1 → 6 条 FAIL; +> gap 阈值改 `>2` → 1 条 FAIL → 证明这些用例真的咬得住。 + +### 2.5 取数与参数守卫(`adc_input_raw`) + +| 用例 | 断言 | 验证的设计点 | +|---|---|---| +| **TC-AI-040** | `out == NULL` → -1 | 参数防御 | +| **TC-AI-041** | `ch == ADC_SEM_COUNT` → -1 | 越界防御 | +| **TC-AI-042** | `ch == (adc_semantic_t)-1` → -1 | 负数兜底 | +| **TC-AI-043** | 参数错时**不写** `*out` | 无副作用 | +| **TC-AI-044** | TPAFE 无有效帧 → -2 | 有效标志语义 | +| **TC-AI-045** | STM32 无有效扫描 → -2 | 有效标志语义 | +| **TC-AI-049** | `UA` → `buf[6]` 的值(注入 3006) | 按**下标**取数(非按枚举序) | +| **TC-AI-04A** | `UN` → `buf[5]` 的值(注入 2005) | 同上 | +| **TC-AI-04B** | `IA` → `buf[2]` 的值(注入 **-1234**) | 16 位补码负值原样保留(不截断 / 不取绝对值) | + +### 2.6 采集一拍(`adc_input_poll`) + +| 用例 | 断言 | 验证的设计点 | +|---|---|---| +| **TC-AI-046** | 快照返回 1 → `poll()` 返回 1 | 返回码透传 | +| **TC-AI-047** | 快照被请求 **8** 通道 | 与 `ADC_INPUT_TPAFE_CH_NUM` 一致 | +| **TC-AI-048** | 快照目的缓冲非空 | 不传 NULL 给驱动 | +| **TC-AI-050** | 快照返回 0(无新帧)→ `poll()` 返回 0 | 无新帧语义 | +| **TC-AI-051** | 无新帧时 TPAFE 侧无效 | 不置有效标志 | +| **TC-AI-052** | 同拍 STM32 扫描**照常执行**(`rc == RC_OK`) | 两侧独立 | +| **TC-AI-053** | 快照返回 -1(参数错)→ `poll()` 返回 -1 | 错误透传 | +| **TC-AI-054** | 参数错时 TPAFE 侧无效 | 不置有效标志 | +| **TC-AI-055** | 已有有效帧后再"无新帧" → **保持**上次有效值 | 有效标志只置不清 | +| **TC-AI-056** | STM32 失败**不影响** `poll()` 返回值(仍为 1) | 两侧解耦 | +| **TC-AI-057** | STM32 失败不影响 TPAFE 取数 | 同上 | +| **TC-AI-058** | STM32 失败另由 `adc_input_stm32_rc()` 暴露 | 故障通道分离 | + +### 2.7 状态码与失败计数观测 + +| 用例 | 断言 | +|---|---| +| **TC-AI-060** | init 后未取帧 → `RC_NOT_RUN(-3)` | +| **TC-AI-061** | init 后各计数 = 0 | +| **TC-AI-062** | 一次成功后 `rc == OK` 且 `errs` 不变 | +| **TC-AI-063** | 连续两拍无新帧 → `errs == 1`(单拍伪影不计) | +| **TC-AI-064** | `rc` = **最近一次**(`RC_STALL`) | +| **TC-AI-065** | 恢复成功后 `rc == OK` | +| **TC-AI-066** | `errs` **单调不回退**(不因成功回落) | +| **TC-AI-067** | `seq` 只由 DMA 完成回调推进(消费动作不改它) | + +**当前结果**:**101/101 PASSED**(`failed=0`)。 + +--- + +## 3. 如何扩展用例 + +1. 在 `adc_input_test.c` 加一个 `static void tc_xxx(void)`; +2. 用例开头调 `fakes_reset()`(把全部桩复位到"健康"初值 + 复位观测计数); +3. 用 `s_start_rc` / `s_poll_fail_at` / `s_poll_vals[]` 注入 HAL 行为; + 用 `s_snapshot_rc` / `s_fake_tpafe[]` 注入 TPAFE 帧; +4. `CHECK(cond, "描述")` 断言;在 `main()` 里注册(并同步更新本文档 §2 与变更记录)。 + +> ⚠ `fakes_reset()` 必须在新用例开头调用:桩计数器(`s_poll_calls` / `s_rank_idx` / +> `s_start_calls` / `s_stop_calls` / `s_snapshot_calls`)跨用例共享,不复位会互相污染。 +> +> ⚠ 模块静态态(`s_stm32_raw` / `s_tpafe_raw` / `s_*_valid` / `s_stm32_rc` / `s_stm32_errs`) +> 由 `adc_input_init()` 复位;**每个用例开头都应调一次 `adc_input_init()`**(等价"刚上电")。 + +--- + +## 4. 已知边界与设计取舍 + +### 4.1 `map_check()` 反向路径结构上不可达 + +`map_check()` 的失败分支要求"映射表指向 TPAFE 下标 ≥8,或 STM32 通道号不在扫描 Rank 内"。 +但 `k_adc_map[]` 与 `k_stm32_rank_ch[]` 都是**编译期 `static const`**,没有任何运行时输入 +能使其不自洽(曾尝试用 `#define` 重定向表名,会与生产文件内的定义撞成重复定义)。 +因此本单测**只覆盖正向路径**(`map_check() == 0`);反向路径的等效保障是 +`adc_input_init()` 的返回值——它在开机时自检并留痕(见 `app_main.c`)。 + +### 4.2 本层不做标定(L1-5 待办) + +`adc_input_raw()` 返回的是**未标定原始码值**(TPAFE = 16 位补码,STM32 = 0..4095)。 +一次系数 / 零漂标定属 **L1-5,尚未实现**,故本单测不对"物理量换算"做任何断言。 + +### 4.3 不覆盖 V0.1 的硬件缺陷(N-18) + +V0.1 上 8 路电气量分属两片转换器且**无共同采样触发**(N-18),本层只做**逐通道独立监视**。 +"跨转换器相量合成"(线 / 负序 / 零序)不得在本层或 L2 上做——属 L2-5 且 V0.1 受限 +(见 `docs/算法/继电保护功能复现规划.md` §1.6)。 + +--- + +## 5. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-10 | 创建;70 用例全绿(`/W4` 0 告警);覆盖 `adc_input.c` 全部 8 个函数(6 公开 + 2 静态);挂 pre-commit 钩子 `adc-input-unit-test` | +| 2026-10-10 | **新增 TC-AI-070..078(70 → 79 用例)**:钉死"失败拍不得沿用上一拍旧值"。配套修复 `App/algo/adc_input.c` 两条失败支路(清 `s_stm32_valid`)。已做**反向验证**:改回旧行为 → TC-AI-039b/073/074 立即 FAIL(76/79),证明用例有效 | +| 2026-10-10 | **v2.0 DMA 模型改造(79 → 101 用例)**:被测代码 `stm32_scan()`(逐通道 `PollForConversion`)换成 `stm32_take()`(消费 DMA 乒乓半区)。桩面同步:删 `HAL_ADC_Start/PollForConversion/GetValue/Stop`,加 `HAL_ADC_Start_DMA` 与 `hadc_other`;"DMA 完成"由测试直接调用**被测代码自己实现**的三个 HAL 弱回调注入(`dma_beat()`)。新增三组:⭐ **TC-AI-080..089** 乒乓半区(往正在写的另一半填干扰值仍须读到刚完成那一半)+ 超拍丢帧;**TC-AI-090..09A** HAL 错误回调与 `hadc == &hadc1` 句柄守卫;**TC-AI-03A..03E / 067** 停摆(连续 ≥2 拍)与丢帧(`seq` 前进 >1)分开计数。TC-AI-070..078 陈旧帧组**原样保留**(触发条件由"轮询超时"变"本拍无新帧",同一防线)。反向验证四连:半区奇偶取反 → 11 条 FAIL;停摆拍置 `valid=1` → 4 条 FAIL;`STALL_RUN_MAX` 改 1 → 6 条 FAIL;gap 阈值改 `>2` → 1 条 FAIL | diff --git a/docs/测试说明/CH395F_Test_Guide.md b/docs/测试说明/CH395F_Test_Guide.md index 894baa57..c37998fa 100644 --- a/docs/测试说明/CH395F_Test_Guide.md +++ b/docs/测试说明/CH395F_Test_Guide.md @@ -2,7 +2,7 @@ ## 1. 概述 -本文档定义 CH395F 以太网驱动(STM32F407ZGTx,SPI2 接口)的测试规范。当前固件运行于**多 Socket 模式**(`FUN_PARA=0x09`:8 个 Socket 0~7 全部可用,Socket 0 作 TCP Server 单连接),覆盖单连接与多并发客户端场景。测试按阶段(TM-PHY-NN)组织,每个阶段包含独立测试用例(TC-NET-NNN)。 +本文档定义 CH395F 以太网驱动(STM32F407ZGTx,SPI2 接口)的测试规范。当前固件 `FUN_PARA=0x08`(bit3 SOCKET_CLOSE=1,bit0/bit1 均为 0):8 个 Socket 0~7 全部可用(**由 Socket 索引 `_SN` 命令族 + 缓冲显式分配实现,与 FUN_PARA 无关**,见 `问题记录/CH395F_Trap_Records.md` Trap 18「定案更正」),Socket 0 作 TCP Server 单连接,覆盖单连接与多并发客户端场景。测试按阶段(TM-PHY-NN)组织,每个阶段包含独立测试用例(TC-NET-NNN)。 ### 设计原则 @@ -17,8 +17,8 @@ | 参数 | 值 | 说明 | |------|-----|------| -| CH395F 内部总缓冲 | **24KB(48 块 × 512B)** | 手册 §5.45;CH395F.md "内置 24K RAM"。**经 Phase 7 实测确认块 0~47 全部有效**(此前 32×1KB 记录有误) | -| 上电默认分配 | Socket 0~3 各 接收8块(4KB)+发送4块(2KB);Socket 4~7 未分配 | 见 §5.45,用到 >4 个 Socket 必须重分配 | +| CH395F 内部总缓冲 | **24KB(48 块 × 512B)** | `docs/芯片手册/CH395F.md` §5.45;CH395F.md "内置 24K RAM"。**经 Phase 7 实测确认块 0~47 全部有效**(此前 32×1KB 记录有误) | +| 上电默认分配 | Socket 0~3 各 接收8块(4KB)+发送4块(2KB);Socket 4~7 未分配 | 见 `docs/芯片手册/CH395F.md` §5.45,用到 >4 个 Socket 必须重分配 | | **本固件分配(net_init)** | 8 个 Socket 各 **接收 4 块(2KB) + 发送 2 块(1KB)** | 块数由 `CH395F_TCP_MSS` 推导(接收 `CH395F_RECV_BLOCKS`=2×MSS、发送 `CH395F_SEND_BLOCKS`=1×MSS,每块 512B);起始块 `recv=i*6`、`send=i*6+4`;用尽 0~47,互不重叠 | | **TCP MSS** | **1024** | 由接收缓冲(2KB) ≥ 2×MSS 决定,8 Socket 共享 24KB 时的最大值 | | **SPI DMA 缓冲** | **4100B** | `CH395F_SPI_DMA_BUF_SIZE` | @@ -29,7 +29,7 @@ ### 多 Socket / 8 端口并发分配 -`net_init` 中 `ch395f_set_fun_para(0x09)` 启用 8 个 Socket 后,循环 0~7 调用 `SET_RECV_BUF` / `SET_SEND_BUF` 重分配(必须在 `OPEN_SOCKET` 之前,顺序遵循 Trap 01:先数据 Socket 1~7 再监听 Socket 0): +`net_init` 中 `ch395f_set_fun_para(0x08)` 之后,循环 0~7 调用 `SET_RECV_BUF` / `SET_SEND_BUF` 重分配(**这一步才是 Socket 1~7 可用的前提**,见 `问题记录/CH395F_Trap_Records.md` Trap 18「定案更正」;必须在 `OPEN_SOCKET` 之前,顺序遵循 Trap 01:先数据 Socket 1~7 再监听 Socket 0): | Socket | 接收起始块 | 接收块数 | 接收大小 | 发送起始块 | 发送块数 | 发送大小 | |--------|-----------|---------|---------|-----------|---------|---------| @@ -227,6 +227,33 @@ MCU: STM32F407ZGTx @ 168MHz, Keil MDK-ARM v5 (ARMCC) --- +### TC-NET-204(硬件层): TCP 关闭次序 A/B/C 对照(Trap 07 定案用例) + +| 字段 | 值 | +|------|-----| +| **ID** | TC-NET-204 | +| **优先级** | P1 | +| **类型** | 机理对照(硬件层,2026-10-11 新增) | +| **标题** | `close` / `disconnect→close` / `disconnect` 三种关闭次序的芯片行为 | +| **前置条件** | 1. PC: `python test/host/ch395f_socket_test.py tcp_server --port 8081 --timeout 900`(⚠ 默认 `--timeout 180` 且连接间隔 >60 s 会自行退出,长轮次须放大)
2. TC-NET-201 通过;`FUN_PARA=0x08`(`net_stack_bring_up` 与生产同构) | +| **规模** | 3 模式 × 5 轮,每轮 3 项判定(会话 / 关闭次序 / 重开会话),共 45 项 | + +**测试步骤与判定标准**(每轮:`open`→`connect`→64 B 回显比对 → 按模式执行关闭次序 → 重开重连回显): + +| 模式 | 关闭序列 | 成功标准 | 实测(2026-10-11,5/5) | +|----|------|----------|----------| +| A | `ch395f_close_socket()` | 观察窗 10 s 内 `GET_SOCKET_STATUS` **byte0** 回到 `0x00` | **0 / 0 / 150 / 0 / 0 ms**;过渡态可见 `TIME_WAIT`(`0x0A`),**无 RST 语义** | +| B | `ch395f_tcp_disconnect()` → `ch395f_close_socket()` | 同上 | **全部 +0 ms**,两字节均 `0x00` → Trap 07 的"卡 `FIN_WAIT_2` 数分钟"**不成立** | +| C | `ch395f_tcp_disconnect()`(**不** close) | **预期相反**:10 s 内 byte0 保持 `0x05`(`docs/芯片手册/CH395F.md` §5.52 ② bit3=1 芯片不自动关) | byte0 恒 `0x05`、byte1 `0x00`;直接 `open_socket` → **`0x20` FAIL**(5/5,即历史 `Phase 2 open_socket: 0x20` 签名);补 `close` 后 +0 ms 回 CLOSED | + +**判据口径**:`GET_SOCKET_STATUS_SN` 输出 2 字节——byte0 只有 `00H/05H`,byte1 才是 TCP 态且**仅在 byte0=OPEN 时有意义**(`docs/芯片手册/CH395F.md` §5.22 + 官方 SDK 注释);关闭与重开之间**不主动清中断**,残留事件由重开后首次中断读取现场暴露(本轮 15 轮均未观察到残留 `DISCONNECT`)。 + +**通过标准**:45/45。任一模式偏离上表 → 说明芯片关闭语义或 `FUN_PARA` 配置变了,须回查 `问题记录/CH395F_Trap_Records.md` Trap 07「定案更正」。 + +**覆盖原则**:DP-03(状态机边界:关闭次序 × Socket 复用) + +--- + ## 4. 阶段 3:UDP Server Echo(CH395F 作为 UDP Server) | 阶段 ID | TM-PHY-03 | @@ -323,7 +350,7 @@ CH395F PC (dhcp_server) |----|------|------| | 1 | `ch395f_set_dhcp(1)` 启动协商 | - | | 2 | 轮询 `GET_DHCP_STATUS == 0x00` | 超时 30s,间隔 500ms(循环中同步读清 GINT_ALL) | -| 3 | `GET_IP_INF` 轮询至连续 3 次一致(间隔 300ms,上限 8s)后打印 | 连续一致且非 `0.0.0.0`(见风险 #4) | +| 3 | `GET_IP_INF` 轮询至连续 3 次一致(间隔 300ms,上限 8s)后打印 | 连续一致且非 `0.0.0.0`(滞后窗口实测 ~1 s,见 Trap 15) | | 4 | Socket 6(UDP,本地 :60000,send/recv buf 各 512B)向 `255.255.255.255:60001` 广播 4B 本机 IP 宣告 | SENBUF_FREE 等待 2s | | 5 | 同 Socket 收 PC `"HELLO"`(5B),按包头源地址回显一次后关闭 | 窗口 30s(`PHASE4_HELLO_WAIT_MS`) | @@ -388,7 +415,7 @@ CH395F PC (dhcp_server) | **出口** | 串口 "Phase 5: N/N PASSED (failed=0)" + 吞吐统计 | | **注意** | 与 NET 层阶段 7 同主题不同层:本阶段用 `ch395f_*` 裸命令直驱芯片,不经 net_socket/netTask | -> 编号说明:阶段 5 用例 **501/502**(顺序双测向完整性 + 变总量合规);原 PHY 测试(旧编号 901/902)因板载拓扑不可测已删除(见风险表 #1)。 +> 编号说明:阶段 5 用例 **501/502**(顺序双测向完整性 + 变总量合规);原 PHY 测试(旧编号 901/902)因板载拓扑不可测已删除(见 Trap 05)。 ### 为什么 2KB 接收缓冲不会丢数据(TCP 流控原理) @@ -834,7 +861,7 @@ sequenceDiagram | **优先级** | P0 | | **类型** | 功能测试 / 并发测试(NET 层) | | **标题** | 同时打开 8 个 Socket 连接 PC Server,各自 64B 收发 | -| **前置条件** | 1. PC: `python ./test/host/ch395f_socket_test.py tcp_server --port 8081 --max-conn 8`(并发回显,`--max-conn` 为并发计数)
2. `net_init()` 已置 `FUN_PARA=0x09`(8 Socket 模式),8 个 Socket 缓冲按「CH395F 缓冲架构」分配(每 Socket 接收 2KB + 发送 1KB,MSS=1024)
3. `ENABLE_PHASE7_TESTS` 已定义 | +| **前置条件** | 1. PC: `python ./test/host/ch395f_socket_test.py tcp_server --port 8081 --max-conn 8`(并发回显,`--max-conn` 为并发计数)
2. `net_init()` 已置 `FUN_PARA=0x08`,8 个 Socket 缓冲按「CH395F 缓冲架构」分配(每 Socket 接收 2KB + 发送 1KB,MSS=1024)——**缓冲显式分配是本用例的前提**(Socket 可用性不依赖 FUN_PARA)
3. `ENABLE_PHASE7_TESTS` 已定义 | **测试步骤与判定标准**: @@ -848,7 +875,9 @@ sequenceDiagram **覆盖原则**:DP-01(多 Socket 并发主路径)、缓冲架构(8×6 块分配,MSS=1024) -> **注**:PC 端 `--max-conn` 须为**并发**计数(连接关闭即减),否则 Phase 7 全量(701+702+704 共 12 连接)会按累计计数超限而拒掉 4~7(见 Trap 19)。该用例已随 Phase 7 实测 **40/40 PASS** 验证。 +> **注**:PC 端 `--max-conn` 须为**并发**计数(连接关闭即减),否则 Phase 7 全量(701+702+704 共 12 连接)会按累计计数超限而拒掉 4~7。该用例已随 Phase 7 实测通过(并发回显落在 sock2/4/5/6/7,见 Trap 18 末)。 +> +> **2026-10-11 复跑(N-31,`FUN_PARA=0x08`)**:**Phase 7 34/34 全通过**,5 路并发 ESTABLISHED + 64 B 回显字节级比对 OK,芯片 Socket 实测落在 **sock2/4/5/6/7**(0/1/3 被 FTP/对时/远显三个监听占用)→ 证明 Socket 可用性不依赖 FUN_PARA。两条**前置排雷**(否则会得到全量假 FAIL):① PC 适配器落在 **Public** 防火墙配置文件会静默丢入站 8081,症状是 TC701/702/704 全部 `CONNECT TIMEOUT`;② 残留的旧 `tcp_server` 进程占着 8081,新服务器绑不上端口不报错。另:`PHASE7_MULTI_COUNT` 须 ≤5(E-23 未修,`NET_MAX_SOCKETS=8` 减去三个常驻监听)。证据链见 `问题记录/CH395F_Trap_Records.md` Trap 18「定案更正」。 --- @@ -1103,7 +1132,7 @@ IDLE → ACCEPT → [SEND: 分块发 → DONE] / [RECV: 分块收+校验 → DON | TRAP-04 | UDP 无 SENDBUF_FREE 等待 | 后续接收挂起 | 每次 write 后等 SENDBUF_FREE | | TRAP-05 | PHY 不兼容 RTL8305NBI | 链路抖动 | `ch395f_set_phy(0x08)` 强制 100M_FULL | | TRAP-06 | KeepAlive 非 500ms 倍数 | 参数不生效 | 必须 500ms 倍数 | -| TRAP-07 | TCP 误用 disconnect | 卡 FIN_WAIT_2 | 用 `close_socket`(RST) 非 `disconnect` | +| TRAP-07 | 只 `disconnect` 不 `close`(bit3=1) | 停在 `0x05/0x00`、重开返 `0x20` | 直接 `close_socket`(§5.35 自动断链,**非 RST**);见 Trap 07「定案更正」与 TC-NET-204 | | TRAP-08 | RECV 电平中断无限循环 | netTask 饿死 | `net_poll` 每次只一批,数据在应用中读 | | TRAP-09 | PHY 刚建立 link 立即 open | 首次连接失败 | 启动延时 + 重试 | | TRAP-10 | 消息队列值拷贝丢失 | API 返回旧值 | 改用 `xTaskNotify` | diff --git a/docs/测试说明/GD5F2GQ5UE_Test_Guide.md b/docs/测试说明/GD5F2GQ5UE_Test_Guide.md index c55c3d8f..377e941b 100644 --- a/docs/测试说明/GD5F2GQ5UE_Test_Guide.md +++ b/docs/测试说明/GD5F2GQ5UE_Test_Guide.md @@ -20,7 +20,7 @@ 因此测试代码可在任意任务上下文调用,但要注意调用任务会被 SPI 传输阻塞数毫秒(擦除最久)。 - **先擦后写**:NAND 只能将 1 写为 0,**写操作前目标块必须已擦除**,否则数据不可预期(见边界条件 TC-GD5F-1003)。 - **ECC 默认开启**:`gd5f2gq5ue_init()` 会置 `Feature(B0h).ECC_EN=1`,读/写均走内部 ECC。 -- **坏块以 BBT 管理**:出厂坏块标记在每块的第一页(page 0)spare byte 0(列地址 0x800,见手册 §12.4 / Table 12-6 / Note);`init` 阶段扫描构建 RAM 中的 BBT。 +- **坏块以 BBT 管理**:出厂坏块标记在每块的第一页(page 0)spare byte 0(列地址 0x800,见 `docs/芯片手册/GD5F2GQ5UExxG.md` §12.4 / Table 12-6 / Note);`init` 阶段扫描构建 RAM 中的 BBT。 ### 芯片参数(关键参数) @@ -706,7 +706,7 @@ ECC 状态(`ECCS1:ECCS0`):`00`=无错;`01/10/11`=纠正 1/2/3 bit;`10` | **测试步骤** | 1. `gd5f2gq5ue_bbt_rebuild()`(关 ECC → 重新扫描出厂坏块 → 开 ECC → `gd5f_bbt_save` 持久化,覆盖注入坏块)
2. `nand_ftl_format()` 清空 dhara map
3. `gd5f_bbt_dump_flash` 校验持久化 BBT 版本 ≥1 | | **通过标准** | 重建返回 OK 且持久化 BBT 版本 ≥1;设备坏块回到出厂集合,注入坏块被清除 | | **注意事项** | 运行一次即恢复;恢复后建议断电重启让 init 重新加载干净 BBT。本 TC 默认关闭,避免常规测试每次重置设备 | -| **依赖** | `gd5f2gq5ue_bbt_rebuild`(详见 §4.8 持久化设计) | +| **依赖** | `gd5f2gq5ue_bbt_rebuild`(详见 §3 坏块管理与 BBT 持久化) | --- diff --git a/docs/测试说明/ST-LINK_CLI_复位使用说明.md b/docs/测试说明/ST-LINK_CLI_复位使用说明.md index 38fcc316..9a8f3fd8 100644 --- a/docs/测试说明/ST-LINK_CLI_复位使用说明.md +++ b/docs/测试说明/ST-LINK_CLI_复位使用说明.md @@ -3,8 +3,8 @@ > 依据:本机 ST-LINK Utility 4.5.0.0 自带 `ST-LINK_CLI.exe`(CLI 版本 v3.5.0.0,`-h` 全文已实测核对)+ ST 官方文档语义。 > 探针路径:经 `DTU_STLINK_CLI` 环境变量解析(本机设为标准安装位 `C:\Program Files (x86)\STMicroelectronics\STM32 ST-LINK Utility\ST-LINK Utility\ST-LINK_CLI.exe`;未设时回退 PATH,`stlink_flash.py` 均可用) > 2026-09-19 已上板实测(ST-LINK/V2,FW V2J37S7,SN `0B250D12A218303030303032`;STM32F407ZGTx 目标,SWD)。 -> 2026-09-20 复测(V0.1 板):烧写静默失败根因闭环、完整启动闭环(详见 §10)。 -> ⚠️ **本机接线:3 根线(SWDIO / SWCLK / GND),nRST 未接** → 能力边界见 §0,实测记录见 §9。 +> 2026-09-20 复测(V0.1 板):烧写静默失败根因闭环(见 §2 ⑤ 与 §7)、完整启动闭环(见 §5)。 +> ⚠️ **本机接线:3 根线(SWDIO / SWCLK / GND),nRST 未接** → 能力边界见 §0,实测记录见 §5。 > 文中标注 ⓞ 的为"官方帮助/文档说法"与"本机实测"不一致处的更正。 --- @@ -108,7 +108,7 @@ ST-LINK_CLI -c SWD -P "MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex" -Rst -Run - ⓞ **`.hex` 直接支持已实测**:1MB Intel HEX 编程耗时 **6.5s**,`Programming Complete`(远快于 UV4/CMSIS-DAP 路径);`.bin` 同理(`fromelf --bin` 可转,ARMCC 自带 `C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe`);`.axf` 直读未测,无需。 - 加 `-V` 编程后校验、`-Q` 静默。 - 与 `stlink_flash.py flash` 同源(都走 ST-LINK_CLI)。原 `download.bat`(`UV4 -f` 走 CMSIS-DAP)与 `reset.bat` **均已删除**(2026-10-10),烧录/复位/回读统一由 `stlink-flash` skill 承载。 -- ⓞ **静默失败模式(2026-09-20 实测,代价最大的坑)**:省略结尾 `-Rst -Run` 的裸 `-c SWD -P <文件>` 仍报 `Memory programmed in 1s 906ms / Programming Complete`,但芯片 flash **不变**(回读向量表 = 旧镜像)——目标核处于死循环/异常状态时,"Normal 连接、不复位"的会话写入不落地;同文件改用完整 `-P <文件> -Rst -Run` 即正常落地(6.5s / 1MB)。**铁律:任何编程命令必须以 `-Rst -Run` 收尾;任何烧录之后必须 `-CmpFile` 全片回读校验**(详见 §10)。 +- ⓞ **静默失败模式(2026-09-20 实测,代价最大的坑)**:省略结尾 `-Rst -Run` 的裸 `-c SWD -P <文件>` 仍报 `Memory programmed in 1s 906ms / Programming Complete`,但芯片 flash **不变**(回读向量表 = 旧镜像)——目标核处于死循环/异常状态时,"Normal 连接、不复位"的会话写入不落地;同文件改用完整 `-P <文件> -Rst -Run` 即正常落地(6.5s / 1MB)。**铁律:任何编程命令必须以 `-Rst -Run` 收尾;任何烧录之后必须 `-CmpFile` 全片回读校验**(详见 §7)。 ### ⑥ 不扰目标连接(HOTPLUG,运行中板子的只读检查)✅ 本机可用 @@ -207,7 +207,7 @@ ST-LINK_CLI -c SWD -P "MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex" -Rst -Run | `stlink_flash.py halt` | `-c SWD -Rst -Halt` | ✅ 复位停核(一次性会话,读状态须同命令行附加;断开后核自动恢复运行) | | `stlink_flash.py run` | `-c SWD -Run` | ✅ 恢复被停核的目标 | | `stlink_flash.py list` | `-List` | ✅ 枚举探针 | -| `stlink_flash.py flash [F]` | `-c SWD -P F -Rst -Run`(F 缺省 = 档案 `hex` = `MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex`) | ✅ 烧写 + 复位 + 运行(1MB ≈ 6.5s;2026-09-20 复测验证,见 §10) | +| `stlink_flash.py flash [F]` | `-c SWD -P F -Rst -Run`(F 缺省 = 档案 `hex` = `MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex`) | ✅ 烧写 + 复位 + 运行(1MB ≈ 6.5s;2026-09-20 复测验证,见 §2 ⑤) | | `stlink_flash.py cmp [F]` | `-c SWD -CmpFile F 0x08000000` | ✅ 全片回读比对(`No difference found` = 真写入) | 退出码:`0`=OK;`1`=探针/目标错误;`2`=CLI 未找到/无效;`3`=用法错误。 @@ -221,8 +221,8 @@ ST-LINK_CLI -c SWD -P "MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex" -Rst -Run | `[RM=xxx]: Unknown debug protocol or option!`(exit=1) | ⓞ v3.5.0.0 CLI 不支持 `RM=`(帮助与实现不符);**且本机 nRST 未接线**——直接用软件复位 `-Rst`(`stlink_flash.py reset`) | | 连接失败 | 目标供电;SWDIO/SWCLK 接线(本机 3 线,勿依赖 nRST 辅助);芯片休眠 → 加 `LPM`;看门狗反复复位 → 提高连接速度(本机无 `UR` 可用);RDP 被锁 → 只能读有限信息 | | 复位后程序没跑 | 读复位向量确认 Flash 有效:`-R32 0x08000000 1`(应指向 0x08xxxxxx,`0xFFFFFFFF`=空片);查 BOOT0 状态;看串口是否有输出 | -| 复位后卡死(串口日志中断在半路) | ⓞ 2026-09-19 实测案例:`-c SWD HOTPLUG -SCore -CoreReg` 读 PC/寄存器定位卡点(案例:V2 引脚固件卡在 0x08000FA0 启动循环,见 §9)——**先定位再下结论**,勿直接怀疑探针 | -| 烧录报成功但芯片行为不变 | ⓞ **先怀疑静默写入失败**(2026-09-20 实测):命令缺 `-Rst -Run`(裸 `-P` 在目标核死循环/异常时不落地)或选项字节异常(RDP 字节非法,见 §10.4)。处置:`-CmpFile` 全片比对/回读向量表首字 → 用 `stlink_flash.py flash` 重烧 → 再 `-CmpFile` 复核;勿直接怀疑探针/接线 | +| 复位后卡死(串口日志中断在半路) | ⓞ 2026-09-19 实测案例:`-c SWD HOTPLUG -SCore -CoreReg` 读 PC/寄存器定位卡点(案例:V2 引脚固件卡在 0x08000FA0 启动循环,见 §5)——**先定位再下结论**,勿直接怀疑探针 | +| 烧录报成功但芯片行为不变 | ⓞ **先怀疑静默写入失败**(2026-09-20 实测):命令缺 `-Rst -Run`(裸 `-P` 在目标核死循环/异常时不落地)或选项字节异常(RDP 字节非法,见 §3 与 §7)。处置:`-CmpFile` 全片比对/回读向量表首字 → 用 `stlink_flash.py flash` 重烧 → 再 `-CmpFile` 复核;勿直接怀疑探针/接线 | | `daemon unresponsive after auto-start`(serial_tool) | ⓞ 复位后几秒内 daemon 忙于排空日志突发;稍等后直接读 `state/serial.log` 文件验证,勿反复重试 | | `port not found: COMx (available: COMy)`(serial_tool) | ⓞ 端口重新枚举(板子重插常见):改 `.serialtest/config.json` → **kill 旧 daemon 进程** → 重新 `open`(daemon 启动时缓存配置,不重启不生效) | | `log --grep` 查不到明明存在的标记 | ⓞ 该子命令实测不可靠(0 命中);改用直接读/`Select-String` 查 `state/serial.log` | diff --git a/docs/测试说明/boot_log_data/20261011_prod_adc_health_boot_log.txt b/docs/测试说明/boot_log_data/20261011_prod_adc_health_boot_log.txt new file mode 100644 index 00000000..cba05672 --- /dev/null +++ b/docs/测试说明/boot_log_data/20261011_prod_adc_health_boot_log.txt @@ -0,0 +1,116 @@ +[+0ms] [----/--/-- --:--:--][APP] boot: syslog init OK +[+0ms] [----/--/-- --:--:--][APP] fw build: Oct 11 2026 00:38:20 +[+10ms] [----/--/-- --:--:--][APP] reset cause: software (NVIC_SystemReset) (CSR=0x14000000) +[+21ms] PREVRUN seg=2 seq=1914766133 t=1240757 +[+21ms] TICKRUN uw=1240801 n=1240801 duw=44 +[+21ms] [----/--/-- --:--:--][RTC] sd2506_init: OK +[+31ms] [----/--/-- --:--:--][APP] SD2506 init: OK (ret=0) +[+31ms] [2026-10-10 17:13:03][CLK] sys_clock_init: RTC initial time 2026-10-10 17:13:03 +[+42ms] [2026-10-10 17:13:03][CLK] sys_clock_init: wall clock base = unix 1791652383, base_tick=137 +[+52ms] [2026-10-10 17:13:03][APP] RS485 init: OK (UART4, DE=PA15) +[+64ms] [2026-10-10 17:13:03][NAND] Resetting NAND... +[+94ms] [2026-10-10 17:13:03][NAND] Reading NAND ID... +[+94ms] [2026-10-10 17:13:03][NAND] NAND ID: MID=0xC8, DID=0x52 +[+94ms] [2026-10-10 17:13:03][NAND] Scanning bad blocks... +[+223ms] [2026-10-10 17:13:03][NAND] BBT scan: 0 bad blocks found +[+234ms] [2026-10-10 17:13:03][NAND] Enabling ECC... +[+234ms] [2026-10-10 17:13:03][NAND] Unlocking block protection... +[+244ms] [2026-10-10 17:13:03][NAND] Locating BBT pool... +[+244ms] [2026-10-10 17:13:03][NAND] Loading persisted BBT... +[+255ms] [2026-10-10 17:13:03][NAND] NAND init OK +[+255ms] [2026-10-10 17:13:03][APP] NAND init: OK +[+255ms] [2026-10-10 17:13:03][ADC] Init OK +[+266ms] [2026-10-10 17:13:03][APP] ADC init: OK +[+266ms] [2026-10-10 17:13:03][APP] ADC input layer init: OK +[+266ms] [2026-10-10 17:13:03][APP] phasor layer init: OK (inst=5) +[+276ms] [2026-10-10 17:13:03][APP] SD2506 ID: 2512153425482405 +[+287ms] [2026-10-10 17:13:03][APP] SD2506 time: 2026-10-10 17:13:04 (week 2) +[+287ms] [2026-10-10 17:13:03][APP] SD2506 temp: 28 C +[+297ms] [2026-10-10 17:13:03][APP] SD2506 battery: 3.11 V +[+297ms] [2026-10-10 17:13:03][APP] App init OK +[+308ms] [2026-10-10 17:13:03][NAND_FTL] Initializing NAND FTL... +[+339ms] [2026-10-10 17:13:03][NAND_FTL] Map resumed: 14094/97504 sectors used +[+339ms] [2026-10-10 17:13:03][NAND_FTL] Map head=117088 tail=99894 epoch=1 +[+349ms] [2026-10-10 17:13:03][NAND_FTL] NAND FTL ready (capacity: 97504 pages = 190 MB) +[+421ms] [2026-10-10 17:13:03] [FS] FATFS mounted +[+473ms] [2026-10-10 17:13:03][SYSLOG] runlog boot entry: power on +[+473ms] [2026-10-10 17:13:03][CFG] register: section 'network' (4 keys) +[+484ms] [2026-10-10 17:13:03][CFG] load: OK (523B, heap free 14728B) +[+494ms] [2026-10-10 17:13:03][NET_CFG] load: OK ip=192.168.1.100 +[+546ms] [2026-10-10 17:13:03][ADC_TASK] first: UA=11429 UB=1026 UC=817 UN=11453 IA=11452 IB=976 IC=1017 IN=865 T=940 +[+556ms] [2026-10-10 17:13:03][ADC_TASK] phasor(first) C01-IA-F1: Re=0 Im=0 rms=0 (x0.1) +[+566ms] [2026-10-10 17:13:03][ADC_TASK] phasor(first) C02-UA-F1: Re=3 Im=0 rms=3 (x0.1) +[+566ms] [2026-10-10 17:13:03][ADC_TASK] phasor(first) C03-UN-F1: Re=0 Im=0 rms=1 (x0.1) +[+577ms] [2026-10-10 17:13:03][ADC_TASK] phasor(first) C04-IA-F2: Re=3 Im=1 rms=3 (x0.1) +[+587ms] [2026-10-10 17:13:03][ADC_TASK] phasor(first) C05-IA-F5: Re=0 Im=-2 rms=2 (x0.1) +[+598ms] [2026-10-10 17:13:03][NET_TASK] === Network Init === +[+598ms] [2026-10-10 17:13:03][NET_TASK] net init: OK +[+598ms] [2026-10-10 17:13:03][NET_TASK] MAC: 3C:AB:72:53:2B:12 +[+608ms] [2026-10-10 17:13:03][NET_TASK] IP_INF: IP=192.168.1.100 GW=192.168.1.1 MASK=255.255.255.0 +[+608ms] MISS start snap=0x00000067 req=0x00000077 t=717 +[+619ms] [2026-10-10 17:13:03][WD] [WD] armed: timeout ~2498ms (worst ~1701ms), miss_tol=800ms +[+629ms] [2026-10-10 17:13:03][UI_TASK] UI task: started, wait net ready +[+2541ms] [2026-10-10 17:13:05][NET_TASK] PHY: 100M FULL +[+2551ms] [2026-10-10 17:13:05][NET_TASK] netTask: started (single-socket mode) +[+2593ms] [2026-10-10 17:13:05] [FTP] waiting for connection... +[+2624ms] [2026-10-10 17:13:05][TIME_SYNC] time_sync: TCP listen on :8888 (sock=1) +[+2634ms] [2026-10-10 17:13:05][UI_TASK] UI task: net ready, init port +[+2645ms] [2026-10-10 17:13:05][UI_TASK] UI task: ui_port_init enter +[+2645ms] [2026-10-10 17:13:05][UI_PORT] theme: mono dark_bg=true applied +[+2655ms] [2026-10-10 17:13:05][UI_TASK] UI port init: OK +[+2655ms] [2026-10-10 17:13:05][UI_TASK] UI task: ui_menu_init enter +[+2666ms] [2026-10-10 17:13:05][UI_MENU] menu build start: total=39408 free=36572 big=36572 +[+2676ms] [2026-10-10 17:13:05][UI_MENU_VIEW] view init: root frame w=66 h=107 +[+2686ms] [2026-10-10 17:13:05][UI_MENU] menu build done: total=39292 free=35708 big=35696 used=3584 +[+2686ms] [2026-10-10 17:13:05][UI_TASK] UI menu init: OK +[+2697ms] [2026-10-10 17:13:05][UI_STANDBY] standby: ready (idle=60000ms) +[+2697ms] [2026-10-10 17:13:05][UI_TASK] UI standby: ready (idle=60s) +[+2749ms] [2026-10-10 17:13:05][UI_TASK] UI net sink: started (port 7003) +[+2749ms] MISS start snap=0x00000067 req=0x00000077 t=2857 +[+2760ms] [PROF] t=2861 uic=0 uip=0 uipn=0 uil=0 uiw=0 uis=0 uisn=0 adc=2455 adcm=1983 adcn=0 gl=0 +[+2770ms] [PROF2] t=2861 defd=11954 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0 +[+4922ms] [PROF] t=5025 uic=213 uip=19574 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=4618 adcm=1983 adcn=0 gl=0 +[+4933ms] [PROF2] t=5025 defd=11954 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0 +[+5335ms] [2026-10-10 17:13:08] [STACK] default peak_used=2152/4096 free_min=1944 (47%) +[+5346ms] [2026-10-10 17:13:08] [STACK] net peak_used=972/3840 free_min=2868 (74%) +[+5356ms] [2026-10-10 17:13:08] [STACK] ftp peak_used=740/4096 free_min=3356 (81%) +[+5356ms] [2026-10-10 17:13:08] [STACK] ui peak_used=4052/16384 free_min=12332 (75%) +[+5366ms] [2026-10-10 17:13:08] [STACK] timesync peak_used=684/2048 free_min=1364 (66%) +[+5377ms] [2026-10-10 17:13:08] [STACK] adc peak_used=952/2560 free_min=1608 (62%) +[+5377ms] [2026-10-10 17:13:08] [STACK] heap free=14576 min_ever=6640 +[+6932ms] [PROF] t=7038 uic=408 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=6631 adcm=1983 adcn=0 gl=0 +[+6943ms] [PROF2] t=7038 defd=11954 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0 +[+8948ms] [PROF] t=9054 uic=608 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=8647 adcm=1983 adcn=0 gl=0 +[+8958ms] [PROF2] t=9054 defd=11954 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0 +[+10304ms] [2026-10-10 17:13:13][ADC_TASK] frames=10002 idle=0 skip=23 seq=10006 frst_err=0 ring=9979 adc1_rc=0 adc1_err=0 adc1_seq=10004 adc1_gap=23 adc1_hw=0 tgap=1 +[+10315ms] [2026-10-10 17:13:13][ADC_TASK] phasor(hb) C01-IA-F1: Re=2 Im=0 rms=2 (x0.1) +[+10325ms] [2026-10-10 17:13:13][ADC_TASK] phasor(hb) C02-UA-F1: Re=1 Im=0 rms=1 (x0.1) +[+10335ms] [2026-10-10 17:13:13][ADC_TASK] phasor(hb) C03-UN-F1: Re=0 Im=0 rms=0 (x0.1) +[+10346ms] [2026-10-10 17:13:13][ADC_TASK] phasor(hb) C04-IA-F2: Re=0 Im=-1 rms=1 (x0.1) +[+10346ms] [2026-10-10 17:13:13][ADC_TASK] phasor(hb) C05-IA-F5: Re=-1 Im=0 rms=1 (x0.1) +[+10956ms] [PROF] t=11057 uic=802 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=10650 adcm=1983 adcn=0 gl=0 +[+10966ms] [PROF2] t=11057 defd=47667 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0 +[+12977ms] [PROF] t=13074 uic=1002 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=12667 adcm=1983 adcn=0 gl=0 +[+12977ms] [PROF2] t=13074 defd=47667 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0 +[+14986ms] [PROF] t=15091 uic=1202 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=14684 adcm=1983 adcn=0 gl=0 +[+14996ms] [PROF2] t=15091 defd=47667 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0 +[+17009ms] [PROF] t=17108 uic=1402 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=16701 adcm=1983 adcn=0 gl=0 +[+17019ms] [PROF2] t=17108 defd=47667 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0 +[+19025ms] [PROF] t=19125 uic=1602 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=18718 adcm=1983 adcn=0 gl=0 +[+19036ms] [PROF2] t=19125 defd=47667 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0 +[+20305ms] [2026-10-10 17:13:23][ADC_TASK] frames=20002 idle=0 skip=25 seq=20006 frst_err=0 ring=19977 adc1_rc=0 adc1_err=0 adc1_seq=20004 adc1_gap=25 adc1_hw=0 tgap=2 +[+20316ms] [2026-10-10 17:13:23][ADC_TASK] phasor(hb) C01-IA-F1: Re=0 Im=0 rms=0 (x0.1) +[+20326ms] [2026-10-10 17:13:23][ADC_TASK] phasor(hb) C02-UA-F1: Re=-2 Im=-2 rms=3 (x0.1) +[+20337ms] [2026-10-10 17:13:23][ADC_TASK] phasor(hb) C03-UN-F1: Re=0 Im=0 rms=1 (x0.1) +[+20337ms] [2026-10-10 17:13:23][ADC_TASK] phasor(hb) C04-IA-F2: Re=-1 Im=-1 rms=1 (x0.1) +[+20348ms] [2026-10-10 17:13:23][ADC_TASK] phasor(hb) C05-IA-F5: Re=2 Im=1 rms=2 (x0.1) +[+21059ms] [PROF] t=21158 uic=1799 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=20751 adcm=1983 adcn=0 gl=0 +[+21069ms] [PROF2] t=21158 defd=47964 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0 +[+23075ms] [PROF] t=23175 uic=1999 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=22768 adcm=1983 adcn=0 gl=0 +[+23085ms] [PROF2] t=23175 defd=47964 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0 +[+25094ms] [PROF] t=25192 uic=2199 uip=58732 uipn=9991 uil=10003 uiw=10337 uis=1 uisn=0 adc=24785 adcm=1983 adcn=0 gl=0 +[+25094ms] [PROF2] t=25192 defd=47964 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0 +[+27102ms] [PROF] t=27209 uic=2399 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=26802 adcm=1983 adcn=0 gl=0 +[+27112ms] [PROF2] t=27209 defd=47964 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0 +[+29122ms] [PROF] t=29226 uic=2599 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=28819 adcm=1983 adcn=0 gl=0 +[+29132ms] [PROF2] t=29226 defd=47964 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0 diff --git a/docs/测试说明/boot_log_data/20261011_prod_phasor_boot_log.txt b/docs/测试说明/boot_log_data/20261011_prod_phasor_boot_log.txt new file mode 100644 index 00000000..a686ec40 --- /dev/null +++ b/docs/测试说明/boot_log_data/20261011_prod_phasor_boot_log.txt @@ -0,0 +1,117 @@ +[+0ms] A-F5: Re=0 Im=1 rms=1 (x0.1) +[+8522ms] [----/--/-- --:--:--][APP] boot: syslog init OK +[+8522ms] [----/--/-- --:--:--][APP] fw build: Oct 11 2026 00:38:20 +[+8532ms] [----/--/-- --:--:--][APP] reset cause: software (NVIC_SystemReset) (CSR=0x14000000) +[+8532ms] PREVRUN seg=2 seq=1914584053 t=10556 +[+8543ms] TICKRUN uw=10569 n=10569 duw=13 +[+8543ms] [----/--/-- --:--:--][RTC] sd2506_init: OK +[+8543ms] [----/--/-- --:--:--][APP] SD2506 init: OK (ret=0) +[+8553ms] [2026-10-10 16:51:42][CLK] sys_clock_init: RTC initial time 2026-10-10 16:51:42 +[+8564ms] [2026-10-10 16:51:42][CLK] sys_clock_init: wall clock base = unix 1791651102, base_tick=137 +[+8564ms] [2026-10-10 16:51:42][APP] RS485 init: OK (UART4, DE=PA15) +[+8585ms] [2026-10-10 16:51:42][NAND] Resetting NAND... +[+8606ms] [2026-10-10 16:51:42][NAND] Reading NAND ID... +[+8606ms] [2026-10-10 16:51:42][NAND] NAND ID: MID=0xC8, DID=0x52 +[+8606ms] [2026-10-10 16:51:42][NAND] Scanning bad blocks... +[+8748ms] [2026-10-10 16:51:42][NAND] BBT scan: 0 bad blocks found +[+8748ms] [2026-10-10 16:51:42][NAND] Enabling ECC... +[+8758ms] [2026-10-10 16:51:42][NAND] Unlocking block protection... +[+8758ms] [2026-10-10 16:51:42][NAND] Locating BBT pool... +[+8758ms] [2026-10-10 16:51:42][NAND] Loading persisted BBT... +[+8769ms] [2026-10-10 16:51:42][NAND] NAND init OK +[+8769ms] [2026-10-10 16:51:42][APP] NAND init: OK +[+8779ms] [2026-10-10 16:51:42][ADC] Init OK +[+8779ms] [2026-10-10 16:51:42][APP] ADC init: OK +[+8790ms] [2026-10-10 16:51:42][APP] ADC input layer init: OK +[+8790ms] [2026-10-10 16:51:42][APP] phasor layer init: OK (inst=5) +[+8800ms] [2026-10-10 16:51:42][APP] SD2506 ID: 2512153425482405 +[+8800ms] [2026-10-10 16:51:42][APP] SD2506 time: 2026-10-10 16:51:42 (week 2) +[+8813ms] [2026-10-10 16:51:42][APP] SD2506 temp: 28 C +[+8813ms] [2026-10-10 16:51:42][APP] SD2506 battery: 3.11 V +[+8823ms] [2026-10-10 16:51:42][APP] App init OK +[+8823ms] [2026-10-10 16:51:42][NAND_FTL] Initializing NAND FTL... +[+8854ms] [2026-10-10 16:51:42][NAND_FTL] Map resumed: 14094/97504 sectors used +[+8854ms] [2026-10-10 16:51:42][NAND_FTL] Map head=116992 tail=99819 epoch=1 +[+8865ms] [2026-10-10 16:51:42][NAND_FTL] NAND FTL ready (capacity: 97504 pages = 190 MB) +[+8936ms] [2026-10-10 16:51:42] [FS] FATFS mounted +[+8988ms] [2026-10-10 16:51:42][SYSLOG] runlog boot entry: reboot +[+8988ms] [2026-10-10 16:51:42][CFG] register: section 'network' (4 keys) +[+8999ms] [2026-10-10 16:51:42][CFG] load: OK (523B, heap free 15240B) +[+9009ms] [2026-10-10 16:51:42][NET_CFG] load: OK ip=192.168.1.100 +[+9030ms] [2026-10-10 16:51:42][ADC_TASK] first: UA=11429 UB=975 UC=840 UN=11454 IA=11451 IB=930 IC=1004 IN=889 T=904 +[+9040ms] [2026-10-10 16:51:42][ADC_TASK] phasor(first) C01-IA-F1: Re=0 Im=-3 rms=3 (x0.1) +[+9051ms] [2026-10-10 16:51:42][ADC_TASK] phasor(first) C02-UA-F1: Re=1 Im=0 rms=1 (x0.1) +[+9062ms] [2026-10-10 16:51:42][ADC_TASK] phasor(first) C03-UN-F1: Re=-1 Im=-1 rms=1 (x0.1) +[+9062ms] [2026-10-10 16:51:42][ADC_TASK] phasor(first) C04-IA-F2: Re=1 Im=2 rms=2 (x0.1) +[+9072ms] [2026-10-10 16:51:42][ADC_TASK] phasor(first) C05-IA-F5: Re=0 Im=0 rms=0 (x0.1) +[+9083ms] [2026-10-10 16:51:42][NET_TASK] === Network Init === +[+9083ms] [2026-10-10 16:51:42][NET_TASK] net init: OK +[+9094ms] [2026-10-10 16:51:42][NET_TASK] MAC: 3C:AB:72:53:2B:12 +[+9094ms] [2026-10-10 16:51:42][NET_TASK] IP_INF: IP=192.168.1.100 GW=192.168.1.1 MASK=255.255.255.0 +[+9104ms] MISS start snap=0x00000067 req=0x00000077 t=686 +[+9104ms] [2026-10-10 16:51:42][WD] [WD] armed: timeout ~2498ms (worst ~1701ms), miss_tol=800ms +[+9115ms] [2026-10-10 16:51:42][UI_TASK] UI task: started, wait net ready +[+11071ms] [2026-10-10 16:51:44][NET_TASK] PHY: 100M FULL +[+11071ms] [2026-10-10 16:51:44][NET_TASK] netTask: started (single-socket mode) +[+11112ms] [2026-10-10 16:51:44] [FTP] waiting for connection... +[+11154ms] [2026-10-10 16:51:44][TIME_SYNC] time_sync: TCP listen on :8888 (sock=1) +[+11154ms] [2026-10-10 16:51:44][UI_TASK] UI task: net ready, init port +[+11164ms] [2026-10-10 16:51:44][UI_TASK] UI task: ui_port_init enter +[+11175ms] [2026-10-10 16:51:44][UI_PORT] theme: mono dark_bg=true applied +[+11175ms] [2026-10-10 16:51:44][UI_TASK] UI port init: OK +[+11185ms] [2026-10-10 16:51:44][UI_TASK] UI task: ui_menu_init enter +[+11185ms] [2026-10-10 16:51:44][UI_MENU] menu build start: total=39408 free=36572 big=36572 +[+11195ms] [2026-10-10 16:51:44][UI_MENU_VIEW] view init: root frame w=66 h=107 +[+11206ms] [2026-10-10 16:51:44][UI_MENU] menu build done: total=39292 free=35708 big=35696 used=3584 +[+11206ms] [2026-10-10 16:51:44][UI_TASK] UI menu init: OK +[+11216ms] [2026-10-10 16:51:44][UI_STANDBY] standby: ready (idle=60000ms) +[+11227ms] [2026-10-10 16:51:44][UI_TASK] UI standby: ready (idle=60s) +[+11268ms] [2026-10-10 16:51:44][UI_TASK] UI net sink: started (port 7003) +[+11279ms] MISS start snap=0x00000067 req=0x00000077 t=2861 +[+11279ms] [PROF] t=2865 uic=0 uip=0 uipn=0 uil=0 uiw=0 uis=0 uisn=0 adc=2458 adcm=2067 adcn=0 gl=0 +[+11289ms] [PROF2] t=2865 defd=11937 defs=7 netp=0 netn=0 adcp=8 adcq=0 gl=0 +[+13444ms] [PROF] t=5028 uic=213 uip=20263 uipn=8995 uil=9999 uiw=10317 uis=1 uisn=0 adc=4621 adcm=2067 adcn=0 gl=0 +[+13455ms] [PROF2] t=5028 defd=11937 defs=7 netp=0 netn=0 adcp=8 adcq=0 gl=0 +[+13858ms] [2026-10-10 16:51:47] [STACK] default peak_used=2152/4096 free_min=1944 (47%) +[+13868ms] [2026-10-10 16:51:47] [STACK] net peak_used=940/3840 free_min=2900 (75%) +[+13868ms] [2026-10-10 16:51:47] [STACK] ftp peak_used=740/4096 free_min=3356 (81%) +[+13879ms] [2026-10-10 16:51:47] [STACK] ui peak_used=4052/16384 free_min=12332 (75%) +[+13889ms] [2026-10-10 16:51:47] [STACK] timesync peak_used=616/2048 free_min=1432 (69%) +[+13889ms] [2026-10-10 16:51:47] [STACK] adc peak_used=928/2048 free_min=1120 (54%) +[+13900ms] [2026-10-10 16:51:47] [STACK] heap free=15088 min_ever=7152 +[+15454ms] [PROF] t=7041 uic=408 uip=58799 uipn=9995 uil=9999 uiw=10317 uis=1 uisn=0 adc=6634 adcm=2067 adcn=0 gl=0 +[+15465ms] [PROF2] t=7041 defd=11937 defs=7 netp=0 netn=0 adcp=8 adcq=0 gl=0 +[+17476ms] [PROF] t=9057 uic=608 uip=58799 uipn=9995 uil=9999 uiw=10317 uis=1 uisn=0 adc=8650 adcm=2067 adcn=0 gl=0 +[+17486ms] [PROF2] t=9057 defd=11937 defs=7 netp=0 netn=0 adcp=8 adcq=0 gl=0 +[+18829ms] [2026-10-10 16:51:52][ADC_TASK] frames=10001 idle=0 skip=24 seq=10006 frst_err=0 ring=9977 adc1_rc=0 adc1_err=0 adc1_seq=10004 adc1_gap=24 adc1_hw=0 +[+18840ms] [2026-10-10 16:51:52][ADC_TASK] phasor(hb) C01-IA-F1: Re=-1 Im=3 rms=3 (x0.1) +[+18850ms] [2026-10-10 16:51:52][ADC_TASK] phasor(hb) C02-UA-F1: Re=-2 Im=-1 rms=3 (x0.1) +[+18850ms] [2026-10-10 16:51:52][ADC_TASK] phasor(hb) C03-UN-F1: Re=-2 Im=0 rms=2 (x0.1) +[+18860ms] [2026-10-10 16:51:52][ADC_TASK] phasor(hb) C04-IA-F2: Re=2 Im=1 rms=3 (x0.1) +[+18860ms] [2026-10-10 16:51:52][ADC_TASK] phasor(hb) C05-IA-F5: Re=2 Im=0 rms=2 (x0.1) +[+19523ms] [PROF] t=11106 uic=807 uip=58799 uipn=9995 uil=9999 uiw=10317 uis=1 uisn=0 adc=10699 adcm=2067 adcn=0 gl=0 +[+19533ms] [PROF2] t=11106 defd=47235 defs=7 netp=0 netn=0 adcp=11 adcq=0 gl=0 +[+21545ms] [PROF] t=13123 uic=1007 uip=58799 uipn=9995 uil=9999 uiw=10317 uis=1 uisn=0 adc=12716 adcm=2067 adcn=0 gl=0 +[+21545ms] [PROF2] t=13123 defd=47235 defs=7 netp=0 netn=0 adcp=11 adcq=0 gl=0 +[+23553ms] [PROF] t=15140 uic=1207 uip=58799 uipn=9995 uil=9999 uiw=10317 uis=1 uisn=0 adc=14733 adcm=2067 adcn=0 gl=0 +[+23563ms] [PROF2] t=15140 defd=47235 defs=7 netp=0 netn=0 adcp=11 adcq=0 gl=0 +[+25575ms] [PROF] t=17157 uic=1407 uip=58799 uipn=9995 uil=9999 uiw=10317 uis=1 uisn=0 adc=16750 adcm=2067 adcn=0 gl=0 +[+25586ms] [PROF2] t=17157 defd=47235 defs=7 netp=0 netn=0 adcp=11 adcq=0 gl=0 +[+27596ms] [PROF] t=19174 uic=1607 uip=58799 uipn=9995 uil=9979 uiw=10317 uis=1 uisn=0 adc=18767 adcm=2067 adcn=0 gl=0 +[+27596ms] [PROF2] t=19174 defd=47235 defs=7 netp=0 netn=0 adcp=11 adcq=0 gl=0 +[+28829ms] [2026-10-10 16:52:02][ADC_TASK] frames=20000 idle=1 skip=26 seq=20006 frst_err=0 ring=19974 adc1_rc=0 adc1_err=0 adc1_seq=20004 adc1_gap=26 adc1_hw=0 +[+28840ms] [2026-10-10 16:52:02][ADC_TASK] phasor(hb) C01-IA-F1: Re=5 Im=1 rms=5 (x0.1) +[+28840ms] [2026-10-10 16:52:02][ADC_TASK] phasor(hb) C02-UA-F1: Re=-1 Im=-1 rms=2 (x0.1) +[+28850ms] [2026-10-10 16:52:02][ADC_TASK] phasor(hb) C03-UN-F1: Re=0 Im=-2 rms=2 (x0.1) +[+28861ms] [2026-10-10 16:52:02][ADC_TASK] phasor(hb) C04-IA-F2: Re=0 Im=1 rms=1 (x0.1) +[+28861ms] [2026-10-10 16:52:02][ADC_TASK] phasor(hb) C05-IA-F5: Re=0 Im=0 rms=0 (x0.1) +[+29622ms] [PROF] t=21206 uic=1804 uip=58799 uipn=9995 uil=9999 uiw=10317 uis=1 uisn=0 adc=20799 adcm=2067 adcn=0 gl=0 +[+29632ms] [PROF2] t=21206 defd=47236 defs=7 netp=0 netn=0 adcp=11 adcq=0 gl=0 +[+31642ms] [PROF] t=23223 uic=2004 uip=58799 uipn=9995 uil=9999 uiw=10317 uis=1 uisn=0 adc=22816 adcm=2067 adcn=0 gl=0 +[+31652ms] [PROF2] t=23223 defd=47236 defs=7 netp=0 netn=0 adcp=11 adcq=0 gl=0 +[+33653ms] [PROF] t=25240 uic=2204 uip=58799 uipn=9995 uil=9999 uiw=10317 uis=1 uisn=0 adc=24833 adcm=2067 adcn=0 gl=0 +[+33663ms] [PROF2] t=25240 defd=47236 defs=7 netp=0 netn=0 adcp=11 adcq=0 gl=0 +[+35670ms] [PROF] t=27257 uic=2404 uip=58799 uipn=9995 uil=9999 uiw=10317 uis=1 uisn=0 adc=26850 adcm=2067 adcn=0 gl=0 +[+35681ms] [PROF2] t=27257 defd=47236 defs=7 netp=0 netn=0 adcp=11 adcq=0 gl=0 +[+37692ms] [PROF] t=29274 uic=2604 uip=58799 uipn=9995 uil=9979 uiw=10317 uis=61 uisn=0 adc=28867 adcm=2067 adcn=0 gl=0 +[+37702ms] [PROF2] t=29274 defd=47236 defs=7 netp=0 netn=0 adcp=11 adcq=0 gl=0 diff --git a/docs/测试说明/boot_log_data/20261012_B6_iec104_roundtrip_log.txt b/docs/测试说明/boot_log_data/20261012_B6_iec104_roundtrip_log.txt new file mode 100644 index 00000000..fef49769 Binary files /dev/null and b/docs/测试说明/boot_log_data/20261012_B6_iec104_roundtrip_log.txt differ diff --git a/docs/测试说明/boot_log_data/20261012_B6_iec104_roundtrip_pre-fix_log.txt b/docs/测试说明/boot_log_data/20261012_B6_iec104_roundtrip_pre-fix_log.txt new file mode 100644 index 00000000..c4e5bd1a Binary files /dev/null and b/docs/测试说明/boot_log_data/20261012_B6_iec104_roundtrip_pre-fix_log.txt differ diff --git a/docs/测试说明/boot_log_data/20261012_B6_prod_boot_log.txt b/docs/测试说明/boot_log_data/20261012_B6_prod_boot_log.txt new file mode 100644 index 00000000..b0106715 --- /dev/null +++ b/docs/测试说明/boot_log_data/20261012_B6_prod_boot_log.txt @@ -0,0 +1,134 @@ +[+0ms] [----/--/-- --:--:--][APP] boot: syslog init OK +[+0ms] [----/--/-- --:--:--][APP] fw build: Oct 12 2026 14:02:47 suite=NONE dstr=off +[+0ms] [----/--/-- --:--:--][APP] reset cause: software (NVIC_SystemReset) (CSR=0x14000000) +[+0ms] PREVRUN seg=2 seq=1933208002 t=1832883 +[+14ms] TICKRUN uw=1832923 n=1832923 duw=40 +[+14ms] [----/--/-- --:--:--][RTC] sd2506_init: OK +[+14ms] [----/--/-- --:--:--][APP] SD2506 init: OK (ret=0) +[+25ms] [2026-10-12 07:21:28][CLK] sys_clock_init: RTC initial time 2026-10-12 07:21:28 +[+35ms] [2026-10-12 07:21:28][CLK] sys_clock_init: wall clock base = unix 1791789688, base_tick=139 +[+35ms] [2026-10-12 07:21:28][APP] RS485 init: OK (UART4, DE=PA15) +[+57ms] [2026-10-12 07:21:28][NAND] Resetting NAND... +[+77ms] [2026-10-12 07:21:28][NAND] Reading NAND ID... +[+77ms] [2026-10-12 07:21:28][NAND] NAND ID: MID=0xC8, DID=0x52 +[+77ms] [2026-10-12 07:21:28][NAND] Scanning bad blocks... +[+212ms] [2026-10-12 07:21:28][NAND] BBT scan: 0 bad blocks found +[+223ms] [2026-10-12 07:21:28][NAND] Enabling ECC... +[+223ms] [2026-10-12 07:21:28][NAND] Unlocking block protection... +[+233ms] [2026-10-12 07:21:28][NAND] Locating BBT pool... +[+233ms] [2026-10-12 07:21:28][NAND] Loading persisted BBT... +[+244ms] [2026-10-12 07:21:28][NAND] NAND init OK +[+244ms] [2026-10-12 07:21:28][APP] NAND init: OK +[+244ms] [2026-10-12 07:21:28][ADC] Init OK +[+254ms] [2026-10-12 07:21:28][APP] ADC init: OK +[+254ms] [2026-10-12 07:21:28][APP] ADC input layer init: OK +[+264ms] [2026-10-12 07:21:28][APP] phasor layer init: OK (inst=5) +[+264ms] [2026-10-12 07:21:28][APP] protection init: OK (set=32 elm=7) +[+275ms] [2026-10-12 07:21:28][APP] SD2506 ID: 2512153425482405 +[+285ms] [2026-10-12 07:21:28][APP] SD2506 time: 2026-10-12 07:21:28 (week 4) +[+285ms] [2026-10-12 07:21:28][APP] SD2506 temp: 31 C +[+285ms] [2026-10-12 07:21:28][APP] SD2506 battery: 3.07 V +[+296ms] [2026-10-12 07:21:28][APP] App init OK +[+307ms] [2026-10-12 07:21:28][NAND_FTL] Initializing NAND FTL... +[+327ms] [2026-10-12 07:21:28][NAND_FTL] Map resumed: 14094/97504 sectors used +[+338ms] [2026-10-12 07:21:28][NAND_FTL] Map head=17696 tail=130306 epoch=2 +[+338ms] [2026-10-12 07:21:28][NAND_FTL] NAND FTL ready (capacity: 97504 pages = 190 MB) +[+410ms] [2026-10-12 07:21:28] [FS] FATFS mounted +[+473ms] [2026-10-12 07:21:28][SYSLOG] runlog boot entry: power on +[+483ms] [2026-10-12 07:21:28][CFG] register: section 'network' (4 keys) +[+483ms] [2026-10-12 07:21:28][CFG] register: section 'ftp' (3 keys) +[+493ms] [2026-10-12 07:21:28][CFG] load: OK (523B, heap free 18088B) +[+493ms] [2026-10-12 07:21:28][NET_CFG] load: OK ip=192.168.1.100 +[+525ms] [2026-10-12 07:21:28][ADC_TASK] first: UA=11428 UB=983 UC=827 UN=11453 IA=11451 IB=902 IC=975 IN=880 T=880 +[+535ms] [2026-10-12 07:21:28][ADC_TASK] phasor(first) C01-IA-F1: Re=-1 Im=-1 rms=1 (x0.1) +[+535ms] [2026-10-12 07:21:28][ADC_TASK] phasor(first) C02-UA-F1: Re=-1 Im=0 rms=1 (x0.1) +[+545ms] [2026-10-12 07:21:28][ADC_TASK] phasor(first) C03-UN-F1: Re=0 Im=-1 rms=1 (x0.1) +[+556ms] [2026-10-12 07:21:28][ADC_TASK] phasor(first) C04-IA-F2: Re=1 Im=-1 rms=1 (x0.1) +[+556ms] [2026-10-12 07:21:28][ADC_TASK] phasor(first) C05-IA-F5: Re=2 Im=2 rms=3 (x0.1) +[+567ms] [2026-10-12 07:21:28][NET_TASK] === Network Init === +[+577ms] [2026-10-12 07:21:28][PROT] protect task start: cycle=20ms elm=7 +[+577ms] [2026-10-12 07:21:28][NET] ch395f ver=0x4A fun_para=0x08 +[+588ms] [2026-10-12 07:21:28][NET_TASK] net init: OK +[+588ms] [2026-10-12 07:21:28][NET_TASK] MAC: 3C:AB:72:53:2B:12 +[+588ms] [2026-10-12 07:21:28][NET_TASK] IP_INF: IP=192.168.1.100 GW=192.168.1.1 MASK=255.255.255.0 +[+598ms] MISS start snap=0x000001E7 req=0x000001F7 t=717 +[+609ms] [2026-10-12 07:21:28][WD] [WD] armed: timeout ~2498ms (worst ~1701ms), miss_tol=800ms +[+619ms] [2026-10-12 07:21:28][UI_TASK] UI task: started, wait net ready +[+2551ms] [2026-10-12 07:21:30][NET_TASK] PHY: 100M FULL +[+2561ms] [2026-10-12 07:21:30][NET_TASK] netTask: started (single-socket mode) +[+2602ms] [2026-10-12 07:21:30] [FTP] using factory default password +[+2633ms] [2026-10-12 07:21:30][TIME_SYNC] time_sync: TCP listen on :8888 (sock=0) +[+2665ms] [2026-10-12 07:21:30][I104] i104: listening on 2404 +[+2665ms] [2026-10-12 07:21:30] [FTP] waiting for connection... +[+2675ms] [2026-10-12 07:21:30][UI_TASK] UI task: net ready, init port +[+2675ms] [2026-10-12 07:21:30][UI_TASK] UI task: ui_port_init enter +[+2686ms] [2026-10-12 07:21:30][UI_PORT] theme: mono dark_bg=true applied +[+2697ms] [2026-10-12 07:21:30][UI_TASK] UI port init: OK +[+2697ms] [2026-10-12 07:21:30][UI_TASK] UI task: ui_menu_init enter +[+2697ms] [2026-10-12 07:21:30][UI_MENU] menu build start: total=39448 free=37004 big=37004 +[+2707ms] [2026-10-12 07:21:30][UI_MENU_VIEW] view init: root frame w=66 h=107 +[+2717ms] [2026-10-12 07:21:30][UI_MENU] menu build done: total=39332 free=36140 big=36128 used=3192 +[+2728ms] [2026-10-12 07:21:30][UI_TASK] UI menu init: OK +[+2728ms] [2026-10-12 07:21:30][UI_STANDBY] standby: ready (idle=60000ms) +[+2738ms] [2026-10-12 07:21:30][UI_TASK] UI standby: ready (idle=60s) +[+2791ms] [2026-10-12 07:21:30][UI_TASK] UI net sink: started (port 7003) +[+2791ms] MISS start snap=0x000001E7 req=0x000001F7 t=2906 +[+2791ms] [PROF] t=2911 uic=0 uip=0 uipn=0 uil=0 uiw=0 uis=0 uisn=0 adc=2496 adcm=2006 adcn=0 gl=0 +[+2801ms] [PROF2] t=2911 defd=135261 defs=7 netp=16 netn=0 adcp=9 adcq=0 gl=0 +[+4911ms] [PROF] t=5025 uic=208 uip=19870 uipn=9996 uil=9999 uiw=10351 uis=1 uisn=0 adc=4610 adcm=2006 adcn=0 gl=0 +[+4921ms] [PROF2] t=5025 defd=135261 defs=7 netp=16 netn=0 adcp=9 adcq=0 gl=0 +[+5324ms] [2026-10-12 07:21:33] [STACK] default peak_used=2152/4096 free_min=1944 (47%) +[+5335ms] [2026-10-12 07:21:33] [STACK] net peak_used=1092/3840 free_min=2748 (71%) +[+5346ms] [2026-10-12 07:21:33] [STACK] ftp peak_used=740/4096 free_min=3356 (81%) +[+5346ms] [2026-10-12 07:21:33] [STACK] ui peak_used=4016/16384 free_min=12368 (75%) +[+5356ms] [2026-10-12 07:21:33] [STACK] timesync peak_used=616/2048 free_min=1432 (69%) +[+5356ms] [2026-10-12 07:21:33] [STACK] adc peak_used=952/2560 free_min=1608 (62%) +[+5366ms] [2026-10-12 07:21:33] [STACK] iec104 peak_used=424/2560 free_min=2136 (83%) +[+5377ms] [2026-10-12 07:21:33] [STACK] protect peak_used=596/2048 free_min=1452 (70%) +[+5387ms] [2026-10-12 07:21:33] [STACK] heap free=17936 min_ever=10000 +[+6935ms] [PROF] t=7053 uic=403 uip=73696 uipn=9996 uil=9999 uiw=10351 uis=1 uisn=0 adc=6638 adcm=2006 adcn=0 gl=0 +[+6946ms] [PROF2] t=7053 defd=135261 defs=7 netp=16 netn=0 adcp=9 adcq=0 gl=0 +[+8951ms] [PROF] t=9069 uic=603 uip=73696 uipn=9996 uil=9999 uiw=10351 uis=1 uisn=0 adc=8654 adcm=2006 adcn=0 gl=0 +[+8961ms] [PROF2] t=9069 defd=135261 defs=7 netp=16 netn=0 adcp=9 adcq=0 gl=0 +[+10306ms] [2026-10-12 07:21:38][ADC_TASK] frames=10001 idle=0 skip=32 seq=10006 frst_err=0 ring=9969 adc1_rc=0 adc1_err=0 adc1_seq=10004 adc1_gap=32 adc1_hw=0 tgap=1 +[+10317ms] [2026-10-12 07:21:38][ADC_TASK] phasor(hb) C01-IA-F1: Re=0 Im=0 rms=1 (x0.1) +[+10327ms] [2026-10-12 07:21:38][ADC_TASK] phasor(hb) C02-UA-F1: Re=0 Im=1 rms=1 (x0.1) +[+10327ms] [2026-10-12 07:21:38][ADC_TASK] phasor(hb) C03-UN-F1: Re=2 Im=2 rms=3 (x0.1) +[+10337ms] [2026-10-12 07:21:38][ADC_TASK] phasor(hb) C04-IA-F2: Re=-2 Im=-2 rms=3 (x0.1) +[+10337ms] [2026-10-12 07:21:38][ADC_TASK] phasor(hb) C05-IA-F5: Re=0 Im=-1 rms=1 (x0.1) +[+10348ms] [2026-10-12 07:21:38][PROT] elm UV-UA asserted=1 picked=1 +[+10957ms] [PROF] t=11070 uic=797 uip=73696 uipn=9996 uil=9999 uiw=10351 uis=1 uisn=0 adc=10655 adcm=2006 adcn=0 gl=0 +[+10968ms] [PROF2] t=11070 defd=135261 defs=7 netp=16 netn=0 adcp=9 adcq=0 gl=0 +[+11371ms] [2026-10-12 07:21:39] [STACK] default peak_used=2152/4096 free_min=1944 (47%) +[+11382ms] [2026-10-12 07:21:39] [STACK] net peak_used=1092/3840 free_min=2748 (71%) +[+11382ms] [2026-10-12 07:21:39] [STACK] ftp peak_used=740/4096 free_min=3356 (81%) +[+11392ms] [2026-10-12 07:21:39] [STACK] ui peak_used=4016/16384 free_min=12368 (75%) +[+11403ms] [2026-10-12 07:21:39] [STACK] timesync peak_used=616/2048 free_min=1432 (69%) +[+11403ms] [2026-10-12 07:21:39] [STACK] adc peak_used=952/2560 free_min=1608 (62%) +[+11413ms] [2026-10-12 07:21:39] [STACK] iec104 peak_used=424/2560 free_min=2136 (83%) +[+11424ms] [2026-10-12 07:21:39] [STACK] protect peak_used=828/2048 free_min=1220 (59%) +[+11434ms] [2026-10-12 07:21:39] [STACK] heap free=17936 min_ever=10000 +[+12987ms] [PROF] t=13099 uic=992 uip=73696 uipn=9996 uil=9999 uiw=10351 uis=1 uisn=0 adc=12684 adcm=2006 adcn=0 gl=0 +[+12997ms] [PROF2] t=13099 defd=135261 defs=7 netp=16 netn=0 adcp=9 adcq=0 gl=0 +[+15001ms] [PROF] t=15116 uic=1192 uip=73696 uipn=9996 uil=9999 uiw=10351 uis=1 uisn=0 adc=14701 adcm=2006 adcn=0 gl=0 +[+15012ms] [PROF2] t=15116 defd=135261 defs=7 netp=16 netn=0 adcp=9 adcq=0 gl=0 +[+17022ms] [PROF] t=17133 uic=1392 uip=73696 uipn=9996 uil=9999 uiw=10351 uis=1 uisn=0 adc=16718 adcm=2006 adcn=0 gl=0 +[+17032ms] [PROF2] t=17133 defd=135261 defs=7 netp=16 netn=0 adcp=9 adcq=0 gl=0 +[+19034ms] [PROF] t=19150 uic=1592 uip=73696 uipn=9996 uil=9999 uiw=10351 uis=1 uisn=0 adc=18735 adcm=2006 adcn=0 gl=0 +[+19044ms] [PROF2] t=19150 defd=135261 defs=7 netp=16 netn=0 adcp=9 adcq=0 gl=0 +[+20307ms] [2026-10-12 07:21:48][ADC_TASK] frames=20001 idle=0 skip=35 seq=20006 frst_err=0 ring=19966 adc1_rc=0 adc1_err=0 adc1_seq=20004 adc1_gap=35 adc1_hw=0 tgap=1 +[+20318ms] [2026-10-12 07:21:48][ADC_TASK] phasor(hb) C01-IA-F1: Re=0 Im=-1 rms=2 (x0.1) +[+20318ms] [2026-10-12 07:21:48][ADC_TASK] phasor(hb) C02-UA-F1: Re=1 Im=-2 rms=2 (x0.1) +[+20328ms] [2026-10-12 07:21:48][ADC_TASK] phasor(hb) C03-UN-F1: Re=2 Im=-1 rms=2 (x0.1) +[+20339ms] [2026-10-12 07:21:48][ADC_TASK] phasor(hb) C04-IA-F2: Re=0 Im=1 rms=1 (x0.1) +[+20339ms] [2026-10-12 07:21:48][ADC_TASK] phasor(hb) C05-IA-F5: Re=2 Im=0 rms=2 (x0.1) +[+21054ms] [PROF] t=21164 uic=1787 uip=73696 uipn=9996 uil=9999 uiw=10351 uis=1 uisn=0 adc=20749 adcm=2006 adcn=0 gl=0 +[+21064ms] [PROF2] t=21164 defd=135261 defs=7 netp=16 netn=0 adcp=9 adcq=0 gl=0 +[+23067ms] [PROF] t=23181 uic=1987 uip=73696 uipn=9997 uil=9950 uiw=10351 uis=64 uisn=0 adc=22766 adcm=2006 adcn=0 gl=0 +[+23077ms] [PROF2] t=23181 defd=135261 defs=7 netp=63 netn=0 adcp=9 adcq=0 gl=0 +[+25080ms] [PROF] t=25198 uic=2187 uip=73696 uipn=9996 uil=9958 uiw=10351 uis=64 uisn=0 adc=24783 adcm=2006 adcn=0 gl=0 +[+25091ms] [PROF2] t=25198 defd=135261 defs=7 netp=63 netn=0 adcp=9 adcq=0 gl=0 +[+27100ms] [PROF] t=27215 uic=2387 uip=73696 uipn=9996 uil=9999 uiw=10351 uis=64 uisn=0 adc=26800 adcm=2006 adcn=0 gl=0 +[+27110ms] [PROF2] t=27215 defd=135261 defs=7 netp=63 netn=0 adcp=9 adcq=0 gl=0 +[+29114ms] [PROF] t=29232 uic=2587 uip=73696 uipn=9996 uil=9999 uiw=10351 uis=64 uisn=0 adc=28817 adcm=2006 adcn=0 gl=0 +[+29125ms] [PROF2] t=29232 defd=135261 defs=7 netp=63 netn=0 adcp=9 adcq=0 gl=0 diff --git a/docs/测试说明/boot_log_data/20261012_B6_prod_boot_log_PROTECT_MISS.txt b/docs/测试说明/boot_log_data/20261012_B6_prod_boot_log_PROTECT_MISS.txt new file mode 100644 index 00000000..ba042d21 --- /dev/null +++ b/docs/测试说明/boot_log_data/20261012_B6_prod_boot_log_PROTECT_MISS.txt @@ -0,0 +1,134 @@ +[+0ms] [----/--/-- --:--:--][APP] boot: syslog init OK +[+0ms] [----/--/-- --:--:--][APP] fw build: Oct 12 2026 14:49:47 suite=NONE dstr=off +[+11ms] [----/--/-- --:--:--][APP] reset cause: software (NVIC_SystemReset) (CSR=0x14000000) +[+11ms] PREVRUN seg=2 seq=1932941145 t=2452628 +[+26ms] TICKRUN uw=2452643 n=2452643 duw=15 +[+26ms] [----/--/-- --:--:--][RTC] sd2506_init: OK +[+26ms] [----/--/-- --:--:--][APP] SD2506 init: OK (ret=0) +[+37ms] [2026-10-12 06:50:17][CLK] sys_clock_init: RTC initial time 2026-10-12 06:50:17 +[+47ms] [2026-10-12 06:50:17][CLK] sys_clock_init: wall clock base = unix 1791787817, base_tick=139 +[+47ms] [2026-10-12 06:50:17][APP] RS485 init: OK (UART4, DE=PA15) +[+68ms] [2026-10-12 06:50:17][NAND] Resetting NAND... +[+79ms] [2026-10-12 06:50:17][NAND] Reading NAND ID... +[+89ms] [2026-10-12 06:50:17][NAND] NAND ID: MID=0xC8, DID=0x52 +[+89ms] [2026-10-12 06:50:17][NAND] Scanning bad blocks... +[+224ms] [2026-10-12 06:50:17][NAND] BBT scan: 0 bad blocks found +[+224ms] [2026-10-12 06:50:17][NAND] Enabling ECC... +[+235ms] [2026-10-12 06:50:17][NAND] Unlocking block protection... +[+245ms] [2026-10-12 06:50:17][NAND] Locating BBT pool... +[+245ms] [2026-10-12 06:50:17][NAND] Loading persisted BBT... +[+256ms] [2026-10-12 06:50:17][NAND] NAND init OK +[+256ms] [2026-10-12 06:50:17][APP] NAND init: OK +[+256ms] [2026-10-12 06:50:17][ADC] Init OK +[+256ms] [2026-10-12 06:50:17][APP] ADC init: OK +[+266ms] [2026-10-12 06:50:17][APP] ADC input layer init: OK +[+266ms] [2026-10-12 06:50:17][APP] phasor layer init: OK (inst=5) +[+277ms] [2026-10-12 06:50:17][APP] protection init: OK (set=31 elm=7) +[+287ms] [2026-10-12 06:50:17][APP] SD2506 ID: 2512153425482405 +[+287ms] [2026-10-12 06:50:17][APP] SD2506 time: 2026-10-12 06:50:17 (week 4) +[+298ms] [2026-10-12 06:50:17][APP] SD2506 temp: 29 C +[+298ms] [2026-10-12 06:50:17][APP] SD2506 battery: 3.09 V +[+308ms] [2026-10-12 06:50:17][APP] App init OK +[+308ms] [2026-10-12 06:50:17][PROT] protect task st[art: cycle=220ms elm=70 +[+319ms] 26-10-12 06:50:17][NAND_FTL] Initializing NAND FTL... +[+340ms] [2026-10-12 06:50:17][NAND_FTL] Map resumed: 14094/97504 sectors used +[+350ms] [2026-10-12 06:50:17][NAND_FTL] Map head=17600 tail=130216 epoch=2 +[+350ms] [2026-10-12 06:50:17][NAND_FTL] NAND FTL ready (capacity: 97504 pages = 190 MB) +[+433ms] [2026-10-12 06:50:17] [FS] FATFS mounted +[+485ms] [2026-10-12 06:50:17][SYSLOG] runlog boot entry: power on +[+485ms] [2026-10-12 06:50:17][CFG] register: section 'network' (4 keys) +[+495ms] [2026-10-12 06:50:17][CFG] register: section 'ftp' (3 keys) +[+506ms] [2026-10-12 06:50:17][CFG] load: OK (523B, heap free 18088B) +[+506ms] [2026-10-12 06:50:17][NET_CFG] load: OK ip=192.168.1.100 +[+537ms] [2026-10-12 06:50:17][ADC_TASK] first: UA=11428 UB=972 UC=825 UN=11454 IA=11451 IB=927 IC=971 IN=878 T=889 +[+537ms] [2026-10-12 06:50:17][ADC_TASK] phasor(first) C01-IA-F1: Re=-1 Im=-2 rms=2 (x0.1) +[+548ms] [2026-10-12 06:50:17][ADC_TASK] phasor(first) C02-UA-F1: Re=2 Im=-1 rms=2 (x0.1) +[+559ms] [2026-10-12 06:50:17][ADC_TASK] phasor(first) C03-UN-F1: Re=-1 Im=1 rms=1 (x0.1) +[+569ms] [2026-10-12 06:50:17][ADC_TASK] phasor(first) C04-IA-F2: Re=3 Im=0 rms=3 (x0.1) +[+569ms] [2026-10-12 06:50:17][ADC_TASK] phasor(first) C05-IA-F5: Re=1 Im=-2 rms=2 (x0.1) +[+579ms] [2026-10-12 06:50:17][NET_TASK] === Network Init === +[+579ms] [2026-10-12 06:50:17][NET] ch395f ver=0x4A fun_para=0x08 +[+590ms] [2026-10-12 06:50:17][NET_TASK] net init: OK +[+590ms] [2026-10-12 06:50:17][NET_TASK] MAC: 3C:AB:72:53:2B:12 +[+600ms] [2026-10-12 06:50:17][NET_TASK] IP_INF: IP=192.168.1.100 GW=192.168.1.1 MASK=255.255.255.0 +[+600ms] MISS start snap=0x000001E7 req=0x000001F7 t=710 +[+611ms] [2026-10-12 06:50:17][WD] [WD] armed: timeout ~2498ms (worst ~1701ms), miss_tol=800ms +[+621ms] [2026-10-12 06:50:17][UI_TASK] UI task: started, wait net ready +[+2605ms] [2026-10-12 06:50:19][NET_TASK] PHY: 100M FULL +[+2625ms] [2026-10-12 06:50:19][NET_TASK] netTask: started (single-socket mode) +[+2657ms] [2026-10-12 06:50:19] [FTP] using factory default password +[+2689ms] [2026-10-12 06:50:19][TIME_SYNC] time_sync: TCP listen on :8888 (sock=0) +[+2720ms] [2026-10-12 06:50:19][I104] i104: listening on 2404 +[+2731ms] [2026-10-12 06:50:19] [FTP] waiting for connection... +[+2731ms] [2026-10-12 06:50:19][UI_TASK] UI task: net ready, init port +[+2741ms] [2026-10-12 06:50:19][UI_TASK] UI task: ui_port_init enter +[+2741ms] [2026-10-12 06:50:19][UI_PORT] theme: mono dark_bg=true applied +[+2752ms] [2026-10-12 06:50:19][UI_TASK] UI port init: OK +[+2752ms] [2026-10-12 06:50:19][UI_TASK] UI task: ui_menu_init enter +[+2762ms] [2026-10-12 06:50:19][UI_MENU] menu build start: total=39448 free=37004 big=37004 +[+2772ms] [2026-10-12 06:50:19][UI_MENU_VIEW] view init: root frame w=66 h=107 +[+2783ms] [2026-10-12 06:50:19][UI_MENU] menu build done: total=39332 free=36140 big=36128 used=3192 +[+2783ms] [2026-10-12 06:50:19][UI_TASK] UI menu init: OK +[+2793ms] [2026-10-12 06:50:19][UI_STANDBY] standby: ready (idle=60000ms) +[+2793ms] [2026-10-12 06:50:19][UI_TASK] UI standby: ready (idle=60s) +[+2845ms] [2026-10-12 06:50:19][UI_TASK] UI net sink: started (port 7003) +[+2855ms] MISS start snap=0x000001E7 req=0x000001F7 t=2954 +[+2855ms] [PROF] t=2959 uic=0 uip=0 uipn=0 uil=0 uiw=0 uis=0 uisn=0 adc=2545 adcm=2017 adcn=0 gl=0 +[+2866ms] [PROF2] t=2959 defd=135841 defs=7 netp=0 netn=0 adcp=9 adcq=0 gl=0 +[+4920ms] [PROF] t=5022 uic=203 uip=20322 uipn=8996 uil=9999 uiw=10487 uis=1 uisn=0 adc=4608 adcm=2017 adcn=0 gl=0 +[+4930ms] [PROF2] t=5022 defd=135841 defs=7 netp=0 netn=0 adcp=9 adcq=0 gl=0 +[+5334ms] [2026-10-12 06:50:22] [STACK] default peak_used=2164/4096 free_min=1932 (47%) +[+5345ms] [2026-10-12 06:50:22] [STACK] net peak_used=1092/3840 free_min=2748 (71%) +[+5345ms] [2026-10-12 06:50:22] [STACK] ftp peak_used=740/4096 free_min=3356 (81%) +[+5355ms] [2026-10-12 06:50:22] [STACK] ui peak_used=4052/16384 free_min=12332 (75%) +[+5365ms] [2026-10-12 06:50:22] [STACK] timesync peak_used=616/2048 free_min=1432 (69%) +[+5365ms] [2026-10-12 06:50:22] [STACK] adc peak_used=952/2560 free_min=1608 (62%) +[+5376ms] [2026-10-12 06:50:22] [STACK] iec104 peak_used=424/2560 free_min=2136 (83%) +[+5387ms] [2026-10-12 06:50:22] [STACK] protect peak_used=456/2048 free_min=1592 (77%) +[+5387ms] [2026-10-12 06:50:22] [STACK] heap free=17936 min_ever=10000 +[+6946ms] [PROF] t=7050 uic=398 uip=73724 uipn=9995 uil=9999 uiw=10487 uis=1 uisn=0 adc=6636 adcm=2017 adcn=0 gl=0 +[+6956ms] [PROF2] t=7050 defd=135841 defs=7 netp=0 netn=0 adcp=9 adcq=0 gl=0 +[+8965ms] [PROF] t=9066 uic=598 uip=73724 uipn=9995 uil=9999 uiw=10487 uis=1 uisn=0 adc=8652 adcm=2017 adcn=0 gl=0 +[+8976ms] [PROF2] t=9066 defd=135841 defs=7 netp=0 netn=0 adcp=9 adcq=0 gl=0 +[+10311ms] [2026-10-12 06:50:27][ADC_TASK] frames=10001 idle=0 skip=32 seq=10006 frst_err=0 ring=9969 adc1_rc=0 adc1_err=0 adc1_seq=10004 adc1_gap=32 adc1_hw=0 tgap=1 +[+10322ms] [2026-10-12 06:50:27][ADC_TASK] phasor(hb) C01-IA-F1: Re=2 Im=2 rms=3 (x0.1) +[+10332ms] [2026-10-12 06:50:27][ADC_TASK] phasor(hb) C02-UA-F1: Re=0 Im=1 rms=1 (x0.1) +[+10343ms] [2026-10-12 06:50:27][ADC_TASK] phasor(hb) C03-UN-F1: Re=-1 Im=0 rms=2 (x0.1) +[+10343ms] [2026-10-12 06:50:27][ADC_TASK] phasor(hb) C04-IA-F2: Re=0 Im=2 rms=2 (x0.1) +[+10353ms] [2026-10-12 06:50:27][ADC_TASK] phasor(hb) C05-IA-F5: Re=1 Im=3 rms=3 (x0.1) +[+10364ms] [2026-10-12 06:50:27][PROT] elm UV-UA asserted=1 picked=1 +[+10962ms] [PROF] t=11068 uic=792 uip=73724 uipn=9995 uil=9958 uiw=10487 uis=1 uisn=0 adc=10654 adcm=2017 adcn=0 gl=0 +[+10972ms] [PROF2] t=11068 defd=135841 defs=7 netp=0 netn=0 adcp=9 adcq=0 gl=0 +[+11384ms] [2026-10-12 06:50:28] [STACK] default peak_used=2164/4096 free_min=1932 (47%) +[+11384ms] [2026-10-12 06:50:28] [STACK] net peak_used=1092/3840 free_min=2748 (71%) +[+11394ms] [2026-10-12 06:50:28] [STACK] ftp peak_used=740/4096 free_min=3356 (81%) +[+11405ms] [2026-10-12 06:50:28] [STACK] ui peak_used=4052/16384 free_min=12332 (75%) +[+11405ms] [2026-10-12 06:50:28] [STACK] timesync peak_used=616/2048 free_min=1432 (69%) +[+11415ms] [2026-10-12 06:50:28] [STACK] adc peak_used=952/2560 free_min=1608 (62%) +[+11425ms] [2026-10-12 06:50:28] [STACK] iec104 peak_used=424/2560 free_min=2136 (83%) +[+11425ms] [2026-10-12 06:50:28] [STACK] protect peak_used=828/2048 free_min=1220 (59%) +[+11436ms] [2026-10-12 06:50:28] [STACK] heap free=17936 min_ever=10000 +[+12998ms] [PROF] t=13097 uic=987 uip=73724 uipn=9995 uil=9995 uiw=10487 uis=1 uisn=0 adc=12683 adcm=2017 adcn=0 gl=0 +[+12998ms] [PROF2] t=13097 defd=135841 defs=7 netp=0 netn=0 adcp=9 adcq=0 gl=0 +[+15007ms] [PROF] t=15114 uic=1187 uip=73724 uipn=9995 uil=9999 uiw=10487 uis=1 uisn=0 adc=14700 adcm=2017 adcn=0 gl=0 +[+15017ms] [PROF2] t=15114 defd=135841 defs=7 netp=0 netn=0 adcp=9 adcq=0 gl=0 +[+17033ms] [PROF] t=17131 uic=1387 uip=73724 uipn=9995 uil=9999 uiw=10487 uis=1 uisn=0 adc=16717 adcm=2017 adcn=0 gl=0 +[+17033ms] [PROF2] t=17131 defd=135841 defs=7 netp=0 netn=0 adcp=9 adcq=0 gl=0 +[+19048ms] [PROF] t=19148 uic=1587 uip=73724 uipn=10003 uil=9999 uiw=10487 uis=1 uisn=0 adc=18734 adcm=2017 adcn=0 gl=0 +[+19058ms] [PROF2] t=19148 defd=135841 defs=7 netp=0 netn=0 adcp=9 adcq=0 gl=0 +[+20309ms] [2026-10-12 06:50:37][ADC_TASK] frames=20001 idle=0 skip=42 seq=20006 frst_err=0 ring=19959 adc1_rc=0 adc1_err=0 adc1_seq=20004 adc1_gap=42 adc1_hw=0 tgap=1 +[+20320ms] [2026-10-12 06:50:37][ADC_TASK] phasor(hb) C01-IA-F1: Re=0 Im=1 rms=1 (x0.1) +[+20330ms] [2026-10-12 06:50:37][ADC_TASK] phasor(hb) C02-UA-F1: Re=3 Im=0 rms=3 (x0.1) +[+20340ms] [2026-10-12 06:50:37][ADC_TASK] phasor(hb) C03-UN-F1: Re=-1 Im=0 rms=1 (x0.1) +[+20351ms] [2026-10-12 06:50:37][ADC_TASK] phasor(hb) C04-IA-F2: Re=-2 Im=-1 rms=2 (x0.1) +[+20351ms] [2026-10-12 06:50:37][ADC_TASK] phasor(hb) C05-IA-F5: Re=0 Im=0 rms=0 (x0.1) +[+21065ms] [PROF] t=21163 uic=1782 uip=73724 uipn=9995 uil=9999 uiw=10487 uis=1 uisn=0 adc=20749 adcm=2017 adcn=0 gl=0 +[+21065ms] [PROF2] t=21163 defd=135841 defs=7 netp=0 netn=0 adcp=9 adcq=0 gl=0 +[+23076ms] [PROF] t=23180 uic=1982 uip=73724 uipn=9995 uil=9999 uiw=10487 uis=1 uisn=0 adc=22766 adcm=2017 adcn=0 gl=0 +[+23086ms] [PROF2] t=23180 defd=135841 defs=7 netp=0 netn=0 adcp=9 adcq=0 gl=0 +[+25093ms] [PROF] t=25197 uic=2182 uip=73724 uipn=9995 uil=9995 uiw=10487 uis=1 uisn=0 adc=24783 adcm=2017 adcn=0 gl=0 +[+25104ms] [PROF2] t=25197 defd=135841 defs=7 netp=0 netn=0 adcp=9 adcq=0 gl=0 +[+27113ms] [PROF] t=27214 uic=2382 uip=73724 uipn=9995 uil=9999 uiw=10487 uis=1 uisn=0 adc=26800 adcm=2017 adcn=0 gl=0 +[+27123ms] [PROF2] t=27214 defd=135841 defs=7 netp=0 netn=0 adcp=9 adcq=0 gl=0 +[+29129ms] [PROF] t=29231 uic=2582 uip=73724 uipn=9995 uil=9999 uiw=10487 uis=1 uisn=0 adc=28817 adcm=2017 adcn=0 gl=0 +[+29140ms] [PROF2] t=29231 defd=135841 defs=7 netp=0 netn=0 adcp=9 adcq=0 gl=0 diff --git a/docs/测试说明/boot_log_data/20261012_B7_iec104_persist_log.txt b/docs/测试说明/boot_log_data/20261012_B7_iec104_persist_log.txt new file mode 100644 index 00000000..54c23368 --- /dev/null +++ b/docs/测试说明/boot_log_data/20261012_B7_iec104_persist_log.txt @@ -0,0 +1,214 @@ +B7 bench leg: IEC104 setpoint persistence + global soft plate (YK12) round-trip +fw: HEAD dafcf80 (B7 1fe163b + B6 gating fix), device 192.168.1.100:2404 +tool: test/host/iec104_master.py --b7-pre / --b7-post / --b7-mark / --b7-check +sequence: pre(reset 前) -> ST-LINK reset -> post; mark(7.5) -> reset -> check +date: 2026-10-12 16:42~16:45 +08:00 + +==== phase 1: --b7-pre (21/21 PASS) ==== +[ 0.006s] iec104 master client -> 192.168.1.100:2404 mode=pre +[ 0.010s] TX U STARTDT act 68 04 07 00 00 00 +[ 0.179s] RX U STARTDT con +[ 1.190s] CHECK startdt_con PASS +[ 1.190s] TX I C_IC act (pre) 68 0E 00 00 00 00 64 01 06 00 01 00 00 00 00 14 +[ 1.281s] RX I type=100 cot=7 body=00 00 00 14 +[ 1.281s] TX S N(R)=1 68 04 01 00 02 00 +[ 1.282s] TX S N(R)=2 68 04 01 00 04 00 +[ 1.283s] RX I M_SP ioa=15 val=0 iv=0 cot=4 [N(R)=1] +[ 1.283s] RX I M_SP ioa=16 val=0 iv=0 cot=4 [N(R)=1] +[ 1.283s] RX I M_SP ioa=17 val=0 iv=0 cot=4 [N(R)=1] +[ 1.283s] RX I M_SP ioa=18 val=0 iv=0 cot=4 [N(R)=1] +[ 1.283s] RX I M_SP ioa=23 val=0 iv=0 cot=4 [N(R)=1] +[ 1.283s] RX I M_SP ioa=31 val=0 iv=0 cot=4 [N(R)=1] +[ 1.283s] TX S N(R)=3 68 04 01 00 06 00 +[ 1.285s] TX S N(R)=5 68 04 01 00 0A 00 +[ 1.385s] RX I type=100 cot=8 body=00 00 00 14 +[ 1.385s] TX S N(R)=6 68 04 01 00 0C 00 +[ 2.400s] CHECK gi_actterm_pre PASS +[ 2.400s] CHECK baseline_yx16_off PASS yx16=(0, False) +[ 2.400s] TX I C_SE select 0x8858 = 7.500 (mark) 68 12 02 00 0C 00 32 01 06 00 01 00 58 88 00 00 00 F0 40 80 +[ 2.487s] RX I type=50 cot=7 body=58 88 00 00 00 F0 40 80 +[ 2.487s] TX S N(R)=7 68 04 01 00 0E 00 +[ 3.497s] CHECK se_sel_mark PASS +[ 3.497s] TX I C_SE exec 0x8858 = 7.500 (mark) 68 12 04 00 0E 00 32 01 06 00 01 00 58 88 00 00 00 F0 40 00 +[ 3.497s] CHECK se_exe_mark PASS +[ 3.497s] TX I C_RD 0x8858 68 0D 06 00 0E 00 66 01 05 00 01 00 58 88 00 +[ 3.588s] RX I type=50 cot=7 body=58 88 00 00 00 F0 40 00 +[ 3.588s] TX S N(R)=8 68 04 01 00 10 00 +[ 3.689s] RX I M_ME_NC ioa=0x8858 val=7.500 qds=0x00 cot=5 +[ 3.689s] TX S N(R)=9 68 04 01 00 12 00 +[ 4.702s] CHECK rd_mark_7.5 PASS read=7.500 want=7.500 +[ 4.702s] TX I C_DC 0x600B select QDC=81 68 0E 08 00 12 00 34 01 06 00 01 00 0B 60 00 81 +[ 4.790s] RX I type=52 cot=7 body=0B 60 00 81 +[ 4.790s] TX S N(R)=10 68 04 01 00 14 00 +[ 5.805s] CHECK yk12_off_sel PASS +[ 5.805s] TX I C_DC 0x600B execute QDC=01 68 0E 0A 00 14 00 34 01 06 00 01 00 0B 60 00 01 +[ 5.805s] CHECK yk12_off_exe PASS +[ 5.805s] TX I C_SE select 0x8858 = 0.050 (low) 68 12 0C 00 14 00 32 01 06 00 01 00 58 88 00 CD CC 4C 3D 80 +[ 5.805s] CHECK se_sel_low PASS +[ 5.805s] TX I C_SE exec 0x8858 = 0.050 (low) 68 12 0E 00 14 00 32 01 06 00 01 00 58 88 00 CD CC 4C 3D 00 +[ 5.805s] CHECK se_exe_low PASS +[ 5.891s] RX I type=52 cot=7 body=0B 60 00 01 +[ 5.891s] TX S N(R)=11 68 04 01 00 16 00 +[ 5.993s] RX I type=50 cot=7 body=58 88 00 CD CC 4C 3D 80 +[ 5.993s] TX S N(R)=12 68 04 01 00 18 00 +[ 6.036s] RX I type=50 cot=7 body=58 88 00 CD CC 4C 3D 00 +[ 6.036s] RX S N(R)=8 +[ 6.036s] TX S N(R)=13 68 04 01 00 1A 00 +[ 9.071s] CHECK plate_out_no_trip PASS YX16 must stay OFF while plate OUT (protection disabled) +[ 9.071s] TX I C_RD 0x8858 68 0D 10 00 1A 00 66 01 05 00 01 00 58 88 00 +[ 9.101s] RX I M_ME_NC ioa=0x8858 val=0.050 qds=0x00 cot=5 +[ 9.101s] TX S N(R)=14 68 04 01 00 1C 00 +[ 10.113s] CHECK rd_low_0.05 PASS read=0.050 want=0.050 +[ 10.113s] TX I C_DC 0x600B select QDC=82 68 0E 12 00 1C 00 34 01 06 00 01 00 0B 60 00 82 +[ 10.113s] CHECK yk12_on_sel PASS +[ 10.113s] TX I C_DC 0x600B execute QDC=02 68 0E 14 00 1C 00 34 01 06 00 01 00 0B 60 00 02 +[ 10.113s] CHECK yk12_on_exe PASS +[ 10.201s] RX I type=52 cot=7 body=0B 60 00 82 +[ 10.201s] TX S N(R)=15 68 04 01 00 1E 00 +[ 10.303s] RX I type=52 cot=7 body=0B 60 00 02 +[ 10.303s] TX S N(R)=16 68 04 01 00 20 00 +[ 10.504s] RX I M_SP ioa=15 val=1 iv=0 cot=3 [N(R)=11] +[ 10.504s] TX S N(R)=17 68 04 01 00 22 00 +[ 10.505s] RX I M_SP ioa=16 val=1 iv=0 cot=3 [N(R)=11] +[ 10.505s] TX S N(R)=18 68 04 01 00 24 00 +[ 11.512s] CHECK spont_yx16_ON PASS +[ 11.512s] CHECK spont_yx15_ON PASS +[ 11.512s] TX I C_SE select 0x8858 = 10.000 (restore) 68 12 16 00 24 00 32 01 06 00 01 00 58 88 00 00 00 20 41 80 +[ 11.607s] RX I type=50 cot=7 body=58 88 00 00 00 20 41 80 +[ 11.607s] TX S N(R)=19 68 04 01 00 26 00 +[ 12.614s] CHECK se_sel_restore PASS +[ 12.614s] TX I C_SE exec 0x8858 = 10.000 (restore) 68 12 18 00 26 00 32 01 06 00 01 00 58 88 00 00 00 20 41 00 +[ 12.614s] CHECK se_exe_restore PASS +[ 12.707s] RX I type=50 cot=7 body=58 88 00 00 00 20 41 00 +[ 12.707s] TX S N(R)=20 68 04 01 00 28 00 +[ 12.708s] RX I M_SP ioa=15 val=0 iv=0 cot=3 [N(R)=13] +[ 12.709s] TX S N(R)=21 68 04 01 00 2A 00 +[ 12.710s] RX I M_SP ioa=16 val=0 iv=0 cot=3 [N(R)=13] +[ 12.710s] TX S N(R)=22 68 04 01 00 2C 00 +[ 13.720s] CHECK yx16_off_after_restore PASS via spontaneous COT=3 +[ 13.720s] TX I C_DC 0x600B select QDC=81 68 0E 1A 00 2C 00 34 01 06 00 01 00 0B 60 00 81 +[ 13.811s] RX I type=52 cot=7 body=0B 60 00 81 +[ 13.811s] TX S N(R)=23 68 04 01 00 2E 00 +[ 14.813s] CHECK yk12_off_final_sel PASS +[ 14.813s] TX I C_DC 0x600B execute QDC=01 68 0E 1C 00 2E 00 34 01 06 00 01 00 0B 60 00 01 +[ 14.813s] CHECK yk12_off_final_exe PASS +[ 14.813s] wait 2s: dirty -> SYS_EVT_PROT_SET_SAVE -> /prot_set.bin +[ 16.813s] SUMMARY: 21/21 checks PASS -> PASS + +==== phase 2: --b7-post (13/13 PASS) ==== +[ 0.007s] iec104 master client -> 192.168.1.100:2404 mode=post +[ 0.024s] TX U STARTDT act 68 04 07 00 00 00 +[ 0.120s] RX U STARTDT con +[ 1.126s] CHECK startdt_con PASS +[ 1.126s] TX I C_IC act (post) 68 0E 00 00 00 00 64 01 06 00 01 00 00 00 00 14 +[ 1.221s] RX I type=100 cot=7 body=00 00 00 14 +[ 1.221s] TX S N(R)=1 68 04 01 00 02 00 +[ 1.223s] TX S N(R)=2 68 04 01 00 04 00 +[ 1.224s] RX I M_SP ioa=15 val=0 iv=0 cot=4 [N(R)=1] +[ 1.224s] RX I M_SP ioa=16 val=0 iv=0 cot=4 [N(R)=1] +[ 1.224s] RX I M_SP ioa=17 val=0 iv=0 cot=4 [N(R)=1] +[ 1.224s] RX I M_SP ioa=18 val=0 iv=0 cot=4 [N(R)=1] +[ 1.224s] RX I M_SP ioa=23 val=0 iv=0 cot=4 [N(R)=1] +[ 1.224s] RX I M_SP ioa=31 val=0 iv=0 cot=4 [N(R)=1] +[ 1.224s] TX S N(R)=4 68 04 01 00 08 00 +[ 1.225s] TX S N(R)=5 68 04 01 00 0A 00 +[ 1.325s] RX I type=100 cot=8 body=00 00 00 14 +[ 1.327s] TX S N(R)=6 68 04 01 00 0C 00 +[ 2.337s] CHECK gi_actterm_post PASS +[ 2.337s] CHECK baseline_yx16_off PASS yx16=(0, False) (plate OUT after reboot -> protection disabled) +[ 2.337s] TX I C_RD 0x8858 68 0D 02 00 0C 00 66 01 05 00 01 00 58 88 00 +[ 2.428s] RX I M_ME_NC ioa=0x8858 val=10.000 qds=0x00 cot=5 +[ 2.428s] TX S N(R)=7 68 04 01 00 0E 00 +[ 3.429s] CHECK persist_rd_10.0 PASS read=10.000 want=10.000 +[ 3.429s] TX I C_SE select 0x8858 = 0.050 (low) 68 12 04 00 0E 00 32 01 06 00 01 00 58 88 00 CD CC 4C 3D 80 +[ 3.528s] RX I type=50 cot=7 body=58 88 00 CD CC 4C 3D 80 +[ 3.528s] TX S N(R)=8 68 04 01 00 10 00 +[ 4.539s] CHECK se_sel_low PASS +[ 4.539s] TX I C_SE exec 0x8858 = 0.050 (low) 68 12 06 00 10 00 32 01 06 00 01 00 58 88 00 CD CC 4C 3D 00 +[ 4.539s] CHECK se_exe_low PASS +[ 4.628s] RX I type=50 cot=7 body=58 88 00 CD CC 4C 3D 00 +[ 4.628s] TX S N(R)=9 68 04 01 00 12 00 +[ 7.649s] CHECK plate_out_persisted_no_trip PASS plate OUT state survived the reset +[ 7.649s] TX I C_DC 0x600B select QDC=82 68 0E 08 00 12 00 34 01 06 00 01 00 0B 60 00 82 +[ 7.731s] RX I type=52 cot=7 body=0B 60 00 82 +[ 7.731s] TX S N(R)=10 68 04 01 00 14 00 +[ 8.734s] CHECK yk12_on_sel PASS +[ 8.734s] TX I C_DC 0x600B execute QDC=02 68 0E 0A 00 14 00 34 01 06 00 01 00 0B 60 00 02 +[ 8.734s] CHECK yk12_on_exe PASS +[ 8.831s] RX I type=52 cot=7 body=0B 60 00 02 +[ 8.831s] TX S N(R)=11 68 04 01 00 16 00 +[ 8.934s] RX I M_SP ioa=15 val=1 iv=0 cot=3 [N(R)=6] +[ 8.934s] TX S N(R)=12 68 04 01 00 18 00 +[ 8.936s] RX I M_SP ioa=16 val=1 iv=0 cot=3 [N(R)=6] +[ 8.936s] TX S N(R)=13 68 04 01 00 1A 00 +[ 9.946s] CHECK spont_yx16_ON_after_reboot PASS +[ 9.946s] TX I C_SE select 0x8858 = 10.000 (restore) 68 12 0C 00 1A 00 32 01 06 00 01 00 58 88 00 00 00 20 41 80 +[ 10.039s] RX I type=50 cot=7 body=58 88 00 00 00 20 41 80 +[ 10.040s] TX S N(R)=14 68 04 01 00 1C 00 +[ 10.539s] RX I M_SP ioa=15 val=0 iv=0 cot=3 [N(R)=7] +[ 10.539s] TX S N(R)=15 68 04 01 00 1E 00 +[ 10.541s] RX I M_SP ioa=16 val=0 iv=0 cot=3 [N(R)=7] +[ 10.541s] TX S N(R)=16 68 04 01 00 20 00 +[ 10.643s] RX I M_SP ioa=15 val=1 iv=0 cot=3 [N(R)=7] +[ 10.643s] TX S N(R)=17 68 04 01 00 22 00 +[ 10.644s] RX I M_SP ioa=16 val=1 iv=0 cot=3 [N(R)=7] +[ 10.644s] TX S N(R)=18 68 04 01 00 24 00 +[ 10.745s] RX I M_SP ioa=15 val=0 iv=0 cot=3 [N(R)=7] +[ 10.745s] TX S N(R)=19 68 04 01 00 26 00 +[ 10.746s] RX I M_SP ioa=16 val=0 iv=0 cot=3 [N(R)=7] +[ 10.746s] TX S N(R)=20 68 04 01 00 28 00 +[ 10.848s] RX I M_SP ioa=15 val=1 iv=0 cot=3 [N(R)=7] +[ 10.848s] TX S N(R)=21 68 04 01 00 2A 00 +[ 10.849s] RX I M_SP ioa=16 val=1 iv=0 cot=3 [N(R)=7] +[ 10.849s] TX S N(R)=22 68 04 01 00 2C 00 +[ 11.250s] RX S N(R)=7 +[ 11.251s] CHECK se_sel_restore PASS +[ 11.251s] TX I C_SE exec 0x8858 = 10.000 (restore) 68 12 0E 00 2C 00 32 01 06 00 01 00 58 88 00 00 00 20 41 00 +[ 11.251s] CHECK se_exe_restore PASS +[ 11.351s] RX I type=50 cot=7 body=58 88 00 00 00 20 41 00 +[ 11.351s] TX S N(R)=23 68 04 01 00 2E 00 +[ 11.351s] RX I M_SP ioa=15 val=0 iv=0 cot=3 [N(R)=8] +[ 11.352s] TX S N(R)=24 68 04 01 00 30 00 +[ 11.352s] RX I M_SP ioa=16 val=0 iv=0 cot=3 [N(R)=8] +[ 11.352s] TX S N(R)=25 68 04 01 00 32 00 +[ 12.360s] CHECK yx16_off_final PASS via spontaneous COT=3 +[ 12.360s] wait 1s: final dirty -> save, leave plate IN +[ 13.360s] SUMMARY: 13/13 checks PASS -> PASS + +==== phase 3: --b7-mark (4/4 PASS) ==== +[ 0.007s] iec104 master client -> 192.168.1.100:2404 mode=mark +[ 0.036s] TX U STARTDT act 68 04 07 00 00 00 +[ 0.166s] RX U STARTDT con +[ 1.172s] CHECK startdt_con PASS +[ 1.172s] TX I C_SE select 0x8858 = 7.500 (distinct) 68 12 00 00 00 00 32 01 06 00 01 00 58 88 00 00 00 F0 40 80 +[ 1.268s] RX I type=50 cot=7 body=58 88 00 00 00 F0 40 80 +[ 1.268s] TX S N(R)=1 68 04 01 00 02 00 +[ 2.273s] CHECK se_sel_distinct PASS +[ 2.273s] TX I C_SE exec 0x8858 = 7.500 (distinct) 68 12 02 00 02 00 32 01 06 00 01 00 58 88 00 00 00 F0 40 00 +[ 2.273s] CHECK se_exe_distinct PASS +[ 2.273s] TX I C_RD 0x8858 68 0D 04 00 02 00 66 01 05 00 01 00 58 88 00 +[ 2.368s] RX I type=50 cot=7 body=58 88 00 00 00 F0 40 00 +[ 2.368s] TX S N(R)=2 68 04 01 00 04 00 +[ 2.469s] RX I M_ME_NC ioa=0x8858 val=7.500 qds=0x00 cot=5 +[ 2.469s] TX S N(R)=3 68 04 01 00 06 00 +[ 3.473s] CHECK rd_distinct PASS read=7.500 want=7.500 +[ 3.473s] wait 1.5s: dirty -> save (/prot_set.bin) +[ 4.973s] SUMMARY: 4/4 checks PASS -> PASS + +==== phase 4: --b7-check (4/4 PASS, non-default 7.5 survived reset) ==== +[ 0.006s] iec104 master client -> 192.168.1.100:2404 mode=check +[ 0.029s] TX U STARTDT act 68 04 07 00 00 00 +[ 0.186s] RX U STARTDT con +[ 1.198s] CHECK startdt_con PASS +[ 1.198s] TX I C_RD 0x8858 68 0D 00 00 00 00 66 01 05 00 01 00 58 88 00 +[ 1.293s] RX I M_ME_NC ioa=0x8858 val=7.500 qds=0x00 cot=5 +[ 1.293s] TX S N(R)=1 68 04 01 00 02 00 +[ 2.297s] CHECK persist_distinct_after_reset PASS read=7.500 want=7.500 +[ 2.297s] TX I C_SE select 0x8858 = 10.000 (restore) 68 12 02 00 02 00 32 01 06 00 01 00 58 88 00 00 00 20 41 80 +[ 2.387s] RX I type=50 cot=7 body=58 88 00 00 00 20 41 80 +[ 2.387s] TX S N(R)=2 68 04 01 00 04 00 +[ 3.391s] CHECK se_sel_restore PASS +[ 3.391s] TX I C_SE exec 0x8858 = 10.000 (restore) 68 12 04 00 04 00 32 01 06 00 01 00 58 88 00 00 00 20 41 00 +[ 3.391s] CHECK se_exe_restore PASS +[ 3.391s] wait 1s: restore saved, bench left at defaults +[ 4.392s] SUMMARY: 4/4 checks PASS -> PASS diff --git a/docs/测试说明/boot_log_data/20261012_B7_persist_boot_log.txt b/docs/测试说明/boot_log_data/20261012_B7_persist_boot_log.txt new file mode 100644 index 00000000..7ca3ca26 --- /dev/null +++ b/docs/测试说明/boot_log_data/20261012_B7_persist_boot_log.txt @@ -0,0 +1,60 @@ +[SYS] 2026-10-12T16:44:51.032+08:00 log cleared (gen=144) +[RX] 2026-10-12T16:44:51.475+08:00 [----/--/-- --:--:--][APP] boot: syslog init OK +[RX] 2026-10-12T16:44:51.475+08:00 [----/--/-- --:--:--][APP] fw build: Oct 12 2026 14:02:47 suite=NONE dstr=off +[RX] 2026-10-12T16:44:51.489+08:00 [----/--/-- --:--:--][APP] reset cause: software (NVIC_SystemReset) (CSR=0x14000000) +[RX] 2026-10-12T16:44:51.489+08:00 PREVRUN seg=2 seq=1933923012 t=123484 +[RX] 2026-10-12T16:44:51.500+08:00 TICKRUN uw=123508 n=123508 duw=24 +[RX] 2026-10-12T16:44:51.500+08:00 [----/--/-- --:--:--][RTC] sd2506_init: OK +[RX] 2026-10-12T16:44:51.511+08:00 [----/--/-- --:--:--][APP] SD2506 init: OK (ret=0) +[RX] 2026-10-12T16:44:51.511+08:00 [2026-10-12 08:44:49][CLK] sys_clock_init: RTC initial time 2026-10-12 08:44:49 +[RX] 2026-10-12T16:44:51.521+08:00 [2026-10-12 08:44:49][CLK] sys_clock_init: wall clock base = unix 1791794689, base_tick=139 +[RX] 2026-10-12T16:44:51.532+08:00 [2026-10-12 08:44:49][APP] RS485 init: OK (UART4, DE=PA15) +[RX] 2026-10-12T16:44:51.543+08:00 [2026-10-12 08:44:49][NAND] Resetting NAND... +[RX] 2026-10-12T16:44:51.564+08:00 [2026-10-12 08:44:49][NAND] Reading NAND ID... +[RX] 2026-10-12T16:44:51.564+08:00 [2026-10-12 08:44:49][NAND] NAND ID: MID=0xC8, DID=0x52 +[RX] 2026-10-12T16:44:51.574+08:00 [2026-10-12 08:44:49][NAND] Scanning bad blocks... +[RX] 2026-10-12T16:44:51.700+08:00 [2026-10-12 08:44:49][NAND] BBT scan: 0 bad blocks found +[RX] 2026-10-12T16:44:51.711+08:00 [2026-10-12 08:44:49][NAND] Enabling ECC... +[RX] 2026-10-12T16:44:51.711+08:00 [2026-10-12 08:44:49][NAND] Unlocking block protection... +[RX] 2026-10-12T16:44:51.721+08:00 [2026-10-12 08:44:49][NAND] Locating BBT pool... +[RX] 2026-10-12T16:44:51.721+08:00 [2026-10-12 08:44:49][NAND] Loading persisted BBT... +[RX] 2026-10-12T16:44:51.732+08:00 [2026-10-12 08:44:49][NAND] NAND init OK +[RX] 2026-10-12T16:44:51.732+08:00 [2026-10-12 08:44:49][APP] NAND init: OK +[RX] 2026-10-12T16:44:51.732+08:00 [2026-10-12 08:44:49][ADC] Init OK +[RX] 2026-10-12T16:44:51.743+08:00 [2026-10-12 08:44:49][APP] ADC init: OK +[RX] 2026-10-12T16:44:51.743+08:00 [2026-10-12 08:44:49][APP] ADC input layer init: OK +[RX] 2026-10-12T16:44:51.753+08:00 [2026-10-12 08:44:49][APP] phasor layer init: OK (inst=5) +[RX] 2026-10-12T16:44:51.753+08:00 [2026-10-12 08:44:49][APP] protection init: OK (set=32 elm=7) +[RX] 2026-10-12T16:44:51.764+08:00 [2026-10-12 08:44:49][APP] SD2506 ID: 2512153425482405 +[RX] 2026-10-12T16:44:51.764+08:00 [2026-10-12 08:44:49][APP] SD2506 time: 2026-10-12 08:44:50 (week 4) +[RX] 2026-10-12T16:44:51.774+08:00 [2026-10-12 08:44:49][APP] SD2506 temp: 28 C +[RX] 2026-10-12T16:44:51.774+08:00 [2026-10-12 08:44:49][APP] SD2506 battery: 3.11 V +[RX] 2026-10-12T16:44:51.785+08:00 [2026-10-12 08:44:49][APP] App init OK +[RX] 2026-10-12T16:44:51.785+08:00 [2026-10-12 08:44:49][NAND_FTL] Initializing NAND FTL... +[RX] 2026-10-12T16:44:51.816+08:00 [2026-10-12 08:44:49][NAND_FTL] Map resumed: 14094/97504 sectors used +[RX] 2026-10-12T16:44:51.816+08:00 [2026-10-12 08:44:49][NAND_FTL] Map head=18496 tail=290 epoch=2 +[RX] 2026-10-12T16:44:51.827+08:00 [2026-10-12 08:44:49][NAND_FTL] NAND FTL ready (capacity: 97504 pages = 190 MB) +[RX] 2026-10-12T16:44:51.899+08:00 [2026-10-12 08:44:49] [FS] FATFS mounted +[RX] 2026-10-12T16:44:51.952+08:00 [2026-10-12 08:44:49][SYSLOG] runlog boot entry: reboot +[RX] 2026-10-12T16:44:51.962+08:00 [2026-10-12 08:44:49][CFG] register: section 'network' (4 keys) +[RX] 2026-10-12T16:44:51.962+08:00 [2026-10-12 08:44:49][CFG] register: section 'ftp' (3 keys) +[RX] 2026-10-12T16:44:51.973+08:00 [2026-10-12 08:44:49][CFG] load: OK (523B, heap free 18088B) +[RX] 2026-10-12T16:44:51.973+08:00 [2026-10-12 08:44:49][NET_CFG] load: OK ip=192.168.1.100 +[RX] 2026-10-12T16:44:51.983+08:00 [2026-10-12 08:44:49] [CFG] prot set loaded (/prot_set.bin) +[RX] 2026-10-12T16:44:52.015+08:00 [2026-10-12 08:44:49][ADC_TASK] first: UA=11428 UB=957 UC=821 UN=11454 IA=11453 IB=938 IC=983 IN=878 T=915 +[RX] 2026-10-12T16:44:52.015+08:00 [2026-10-12 08:44:49][ADC_TASK] phasor(first) C01-IA-F1: Re=-4 Im=1 rms=4 (x0.1) +[RX] 2026-10-12T16:44:52.025+08:00 [2026-10-12 08:44:49][ADC_TASK] phasor(first) C02-UA-F1: Re=-2 Im=0 rms=3 (x0.1) +[RX] 2026-10-12T16:44:52.035+08:00 [2026-10-12 08:44:49][ADC_TASK] phasor(first) C03-UN-F1: Re=-4 Im=-2 rms=5 (x0.1) +[RX] 2026-10-12T16:44:52.046+08:00 [2026-10-12 08:44:49][ADC_TASK] phasor(first) C04-IA-F2: Re=0 Im=0 rms=0 (x0.1) +[RX] 2026-10-12T16:44:52.046+08:00 [2026-10-12 08:44:49][ADC_TASK] phasor(first) C05-IA-F5: Re=0 Im=-1 rms=1 (x0.1) +[RX] 2026-10-12T16:44:52.056+08:00 [2026-10-12 08:44:49][NET_TASK] === Network Init === +[RX] 2026-10-12T16:44:52.056+08:00 [2026-10-12 08:44:49][PROT] protect task start: cycle=20ms elm=7 +[RX] 2026-10-12T16:44:52.067+08:00 [2026-10-12 08:44:49][NET] ch395f ver=0x4A fun_para=0x08 +[RX] 2026-10-12T16:44:52.067+08:00 [2026-10-12 08:44:49][NET_TASK] net init: OK +[RX] 2026-10-12T16:44:52.077+08:00 [2026-10-12 08:44:49][NET_TASK] MAC: 3C:AB:72:53:2B:12 +[RX] 2026-10-12T16:44:52.077+08:00 [2026-10-12 08:44:49][NET_TASK] IP_INF: IP=192.168.1.100 GW=192.168.1.1 MASK=255.255.255.0 +[RX] 2026-10-12T16:44:52.087+08:00 MISS start snap=0x000001E7 req=0x000001F7 t=716 +[RX] 2026-10-12T16:44:52.098+08:00 [2026-10-12 08:44:49][WD] [WD] armed: timeout ~2498ms (worst ~1701ms), miss_tol=800ms +[RX] 2026-10-12T16:44:52.108+08:00 [2026-10-12 08:44:49][UI_TASK] UI task: started, wait net ready +[RX] 2026-10-12T16:44:53.611+08:00 [2026-10-12 08:44:51][PROT] elm OC1-IA asserted=1 picked=1 +[RX] 2026-10-12T16:44:53.673+08:00 [2026-10-12 08:44:51][PROT] elm OC1-IA asserted=0 picked=0 diff --git a/docs/测试说明/boot_log_data/README.md b/docs/测试说明/boot_log_data/README.md index 874be6c0..72df60a2 100644 --- a/docs/测试说明/boot_log_data/README.md +++ b/docs/测试说明/boot_log_data/README.md @@ -27,10 +27,17 @@ |---|---|---|---| | 2026-10-10 | `prod_adc_tim6` / 开机 + 30 s 空载 | `adc` 528/2048(74%);`default` 2164/4096(47%);heap free=15088 min_ever=7152 | `20261010_prod_adc_tim6_boot_log.txt` | | 2026-10-10 | `prod_adc_l1` / 开机 + 30 s 空载 | `adc` **584/2048(71%)**(+56 B,L1 层 adc_input_poll/raw 上链);`default` 2164/4096(47%);heap free=15088 min_ever=7152 | `20261010_prod_adc_l1_boot_log.txt` | +| 2026-10-11 | `prod_phasor` / 开机 + 30 s 空载(L2 台架闭环) | `adc` **928/2048(54%)**(+344 B,压环 + `phasor_run` + `log_phasors` 上链);`default` 2152/4096(47%);heap free=15088 min_ever=7152 | `20261011_prod_phasor_boot_log.txt` | +| 2026-10-11 | `prod_adc_health` / 开机 + 30 s 空载(L1-6 + 抬栈 640 words) | `adc` **952/2560(62%)**(静态最坏链 1080 B,`adc_health_report`→`syslog_post` 回显尾);`default` 2152/4096(47%);heap free=14576 **min_ever=6640**(抬栈 -512 B,符合预算) | `20261011_prod_adc_health_boot_log.txt` | +| 2026-10-12 | `prod`(HEAD d3107f3,B4/B5 验收窗口 B6)/ 开机 + 30 s 空载 | **首采**:`iec104` 424/2560(83%);`protect` 828/2048(59%,free_min 1220);新 56 KB 堆 heap free=17936 **min_ever=10000**(>3072 告警线,无 `[HEAP]` 告警);⚠ **@test FAIL(缺陷证据快照)**:`protect task start` 锚点行字节级交错损坏(`st[art: cycle=220ms elm=70`,两次开机同形复现)= protectTask 违反控制台写者注册前门控纪律(`控制台单写者日志设计说明.md` §3.4),其余 8 Required / 零 `[ERR]` / 锚点×1 / 回读一致全过;**缺陷已修**(启动行改 writer-ready 首拍补打),修复后 PASS 见下行 | `20261012_B6_prod_boot_log_PROTECT_MISS.txt` | +| 2026-10-12 | `prod`(B7 `1fe163b` + B6 门控修复)/ 开机 + 30 s 空载(**B6 复测 PASS**) | `protect` 828/2048(59%,free_min 1220);`iec104` 424/2560(83%);heap free=17936 **min_ever=10000**(**N-33 抬栈 min_ever 背书就绪**);锚点行完好 `cycle=20ms elm=7`(+577 ms,写者 attach 后首拍);零 `[ERR]`、采样闭锁不误报 | `20261012_B6_prod_boot_log.txt` | +| 2026-10-12 | 同 MISS 固件(d3107f3)/ **PC 主站 2404 往返**(`test/host/iec104_master.py`) | STARTDT/总召四组+actterm/C_SE 两段写触发 YX15·16 变位(COT=3)/恢复复归/YK11 两段正答/C_RD 读回 10.000 → **18/18 PASS**(104 链与锚点缺陷无关,验收窗口固件本身上先行闭环) | `20261012_B6_iec104_roundtrip_pre-fix_log.txt` | +| 2026-10-12 | 同 PASS 固件(B7+fix)/ **PC 主站 2404 往返**(复测) | 同上全链 18/18 PASS——B7 `set_sealed`+持久化写路径下 C_SE→YX 联动仍成立(B5 遗留"104 YX 联动"腿闭环) | `20261012_B6_iec104_roundtrip_log.txt` | +| 2026-10-12 | 同 PASS 固件 / **B7 定值持久化 + 软压板 YK12 台架腿**(`test/host/iec104_master.py --b7-pre/--b7-post/--b7-mark/--b7-check`,中间 ST-LINK 复位) | pre **21/21**:C_SE 写 7.5+C_RD 回读、YK12 两段 OFF → 0.05 A 低定值**不动作**(保护退出)、YK12 ON → YX15/16 立即动作(COT=3)、收尾留压板退出;post **13/13**:**压板退出态跨复位保持**(开机日志 `global plate OUT -> protection disabled` + 低定值仍不动作)、重启后 YK12 ON 即动作(联动在位);mark/check **4/4+4/4**:**非默认定值 7.5 跨复位保持**(C_RD=7.500,与出厂默认可区分);开机日志两次 `[CFG] prot set loaded (/prot_set.bin)` | `20261012_B7_iec104_persist_log.txt` / `20261012_B7_persist_boot_log.txt` | ## 4. 关联 - 沉淀约定:`../测试开发标准流程.md` §7.3 - 栈水位探针与基线:`../../内存管理/RAM使用分析与优化方案.md` §0.2 / §0.6 -- 当前诊断基线:`../../待处理内容.md` §6 -- 本流程立项:原 `../../待处理内容.md` S-2(2026-09-22 提出;2026-10-05 固化为本节流程后销项) +- 当前诊断基线:`../../任务规划/基线与待确认.md` §3 +- 本流程立项:原 `../../任务规划/README.md` S-2(2026-09-22 提出;2026-10-05 固化为本节流程后销项) diff --git a/docs/测试说明/m2_data/README.md b/docs/测试说明/m2_data/README.md index 55e5bf59..7d8ed579 100644 --- a/docs/测试说明/m2_data/README.md +++ b/docs/测试说明/m2_data/README.md @@ -1,7 +1,7 @@ # M2 链路延迟实测数据(Phase 13,安静档) 本目录保存 M2(Phase 13:芯片 + 队列时延基线)的**原始实测产物**。判定阈值与解读见 -[`../链路延迟测试说明.md`](../链路延迟测试说明.md) §6.2;缺陷总表见 §8.1。 +[`../链路延迟测试说明.md`](../链路延迟测试说明.md) §6.2;缺陷总表见 `docs/测试说明/链路延迟测试说明.md` §8。 ## 1. 运行元数据 @@ -115,6 +115,6 @@ frame#10:first=16.522ms batches=[1024,1024,1024,137] tail_lag=+55.7ms ## 7. 未纳入本目录的数据 -- run1~run11 为缺陷定位过程(D11~D16 见测试说明 §6.1),其中 run10/run11 的 L7 失败、 +- run1~run11 为缺陷定位过程(D11~D16 见 `docs/测试说明/链路延迟测试说明.md` §6.1),其中 run10/run11 的 L7 失败、 run9 的 170s 连接拒绝均已定位并修复; - 压力档(`--ftp-stress`,E-02 复现)与 `--mode remo`(生产 RemoDispBus 客户端)属 M3。 diff --git a/docs/测试说明/串口助手使用指南.md b/docs/测试说明/串口助手使用指南.md index 12cee8a3..f9c9a51b 100644 --- a/docs/测试说明/串口助手使用指南.md +++ b/docs/测试说明/串口助手使用指南.md @@ -1,7 +1,7 @@ # Python 串口助手(serial_tool)使用指南 > **定位**:面向使用者的操作手册——命令怎么敲、输出怎么看、常见场景怎么组合、出问题怎么排查。 -> **关联文档**:设计决策与实现细节见 `串口助手设计说明.md`;@test 自动化闭环见 `自动化测试流程.md`;`AGENTS.md`「串口调试」节有一页速查。 +> **关联文档**:设计决策与实现细节见 `串口助手设计说明.md`;@test 自动化闭环见 `自动化测试流程.md`;`serial-assistant` skill 有一页速查(用到自动加载,AGENTS.md 不再重复收录)。 > **版本**:v1.1(与 `.dsh/skills/serial-assistant/scripts/serial_tool.py` 当前实现一致,2026-09-15) ## 1. 它是什么(60 秒版) @@ -344,7 +344,7 @@ Get-Content '.dsh\skills\serial-assistant\scripts\state\daemon.log' -Tail 30 # - **daemon 会一直占着内存吗?** 一个 Python 进程约 20MB;不需要时 `stop` 即释放。常驻与否都不影响正确性。 - **daemon 被强杀(taskkill)会怎样?** 端口由系统自动释放;下次任何命令自动拉起新 daemon。**串口不会自动重开**,需要时再 `open`(这是设计:避免在设备异常时盲目重连)。 -- **能同时开两个串口吗?** v1 单口(一个 daemon 管一个口)。多口需求见设计文档 §8 v2 候选。 +- **能同时开两个串口吗?** v1 单口(一个 daemon 管一个口)。多口需求见 `docs/测试说明/串口助手设计说明.md` §8 v2 候选。 - **能直接删 `serial.log` 吗?** 可以,但代际 `gen` 不会变、写句柄不变——建议用 `clear`(原子 + 代际 +1 + 标记行)。 - **为什么 TX 日志里是字面 `\r\n` 而不是真换行?** 日志按行存储;真换行会把一条 TX 劈成两行,转义后一条 TX 恒一行。 - **ST-Link 烧录会抢串口吗?** 不会(烧录走 SWD,不走 COM)——这正是"串口全程保持打开、烧录前后不重开"的设计基础,也彻底绕开了 CH340 释放/重开陷阱。 diff --git a/docs/测试说明/串口助手设计说明.md b/docs/测试说明/串口助手设计说明.md index ed8d2076..613b9b5a 100644 --- a/docs/测试说明/串口助手设计说明.md +++ b/docs/测试说明/串口助手设计说明.md @@ -3,7 +3,7 @@ > **版本**:v1.1(需求 + 设计 + 实施记录) > **日期**:2026-09-15 > **状态**:**已实施**(S1–S6 完成;本机验收 A1–A5/A8 通过,A6/A7 正路径待 CH340 硬件接入后补验) -> **关联文档**:`串口助手使用指南.md`(命令用法/工作流/排障) · `自动化测试流程.md` · `AGENTS.md`(串口调试/自动化测试节) · `.serialtest/config.json`(端口单一配置源,2026-10-10 由 `.opencode/serial_config.json` 迁入) +> **关联文档**:`串口助手使用指南.md`(命令用法/工作流/排障) · `自动化测试流程.md` · `serial-assistant` skill(命令速查/陷阱) · `.serialtest/config.json`(端口单一配置源,2026-10-10 由 `.opencode/serial_config.json` 迁入) > **取代**:`serial-mcp-server_串口调试.md`(实施时删除,工具历史并入本文 §12) ## 1. 背景与目标 diff --git a/docs/测试说明/卷锁单元测试说明.md b/docs/测试说明/卷锁单元测试说明.md new file mode 100644 index 00000000..ac3797a8 --- /dev/null +++ b/docs/测试说明/卷锁单元测试说明.md @@ -0,0 +1,194 @@ +# 卷锁单元测试说明(主机侧) + +> **模块**:Volume Lock(`App/fs/vol_lock.c` + `App/fs/vol_lock.h`) +> **测试类型**:**主机侧纯逻辑单元测试**(L0.5 层)——在 PC 上离线验证 NAND FatFs 卷串行化 +> 互斥锁的创建 / 获取 / 释放契约,零硬件、零烧录、毫秒级、不占 MCU 的 RAM。 +> **状态**:✅ 已落地(33 断言点,`/W4` 0 告警,已挂 pre-commit 钩子) +> **日期**:2026-10-10 +> **关联文档**: +> [`../../AGENTS.md`](../../AGENTS.md)(平台约束与看门狗铁律)、 +> [`测试开发标准流程.md`](测试开发标准流程.md)(测试分层与门禁总览)、 +> [`看门狗单元测试说明.md`](看门狗单元测试说明.md)(同源的喂狗点语义)、 +> [`相量层单元测试说明.md`](相量层单元测试说明.md)(同批落地的姊妹篇)、 +> [`../子系统设计/看门狗设计说明.md`](../子系统设计/看门狗设计说明.md)(`required_mask` 与长耗时白名单) + +--- + +## 1. 概述 + +### 1.1 为什么单独做主机侧单测 + +`vol_lock` 只有 3 个函数、不到 40 行,看起来"不值得"单测。但它的**核心语义是 fail-open**: + +> 互斥量**创建失败**时,`vol_lock_wait()` 必须**返回 1 且绝不阻塞**。 + +这不是一个"顺手写的兜底",而是整个系统**最后一道防复位保险**:`defaultTask` 是看门狗 +喂狗点(`task_wd_poll`),一旦它在等锁上阻塞,主循环即停摆 → 停喂 → IWDG 复位。 +`vol_lock.h` 的文件头把这条写成"使用约定(铁律)",本单测就是它的**回归防线**。 + +真机上 `osMutexNew` 失败**几乎不可能复现**(需 FreeRTOS 堆耗尽),板端套件(`test/board/`) +构造不出这个前置条件。主机侧只需给桩注入一个 `NULL` 返回值即可穷举该分支——这是本单测 +存在的首要理由。 + +次要理由:**幂等性**(重复 `vol_lock_init` 不得重复创建)、**超时值透传**、**未创建时 +`release` 无害**都是纯状态逻辑,主机侧一次钉死。 + +### 1.2 被测对象与桩 + +测试单元**原样 `#include` 生产源文件**(保证测的是生产代码而非副本),平台依赖由桩替身: + +``` +test/host/vol_lock/vol_lock_test.c + ├── #include "cmsis_os.h" ← 桩(test/host/vol_lock/stub/),可注入 创建失败/超时/成功 + └── #include "vol_lock.c" ← 生产源文件(App/fs/),原样纳入 +``` + +| 头文件 | 来源 | 说明 | +|---|---|---| +| `vol_lock.h` | **真实**(`App/fs/`) | 只 `#include `,无平台依赖,无需替身 | +| `cmsis_os.h` | **桩**(`stub/`) | 真实版在 CMSIS-RTOS V2 下把 `osMutexWait` 宏映射到 `osMutexAcquire`;桩直接提供 V1 名 `osMutexWait`,与生产源调用名一致 | + +`include_dirs` 顺序为 **`stub` → `App/fs` → 本目录**:桩目录必须排在前面,否则会被真实 +`cmsis_os.h` 抢先命中。源文件被纳入同一编译单元,故**静态量 `s_lock` 亦可直接观测与置位** +(白盒)。 + +### 1.3 关键约定:fail-open 是"返回 1",不是"返回 0" + +读代码时最容易看错的一点(`vol_lock.c:42-50`): + +| 情形 | `vol_lock_wait()` 返回 | 是否调用 `osMutexWait` | 调用方该做什么 | +|---|---|---|---| +| 已创建 + 取得锁 | `1` | 是(`osOK`) | 正常进入临界区,事后 `release` | +| 已创建 + 超时 | `0` | 是(非 `osOK`) | **不得** `release`;本轮跳过 | +| **未创建 + 创建失败** | **`1`** | **否(零阻塞)** | **照常进入临界区**(无保护),事后 `release` 判空静默 | + +即:**返回 1 = "你可以往下走"**,其代价是"可能没有真正持锁"。这保证了喂狗点在任何情况下 +都不会被锁阻塞。`TC-VL-008`(返回 1)与 `TC-VL-008b`(`osMutexWait` 调用次数 == 0) +两条断言共同钉死该语义——只断言返回 1 是不够的,必须同时证明**没有进入任何等待路径**。 + +> `vol_lock_init()` 的返回值与 `wait()` 的语义**无关**:`init` 失败返回 `-1`,但 +> `wait()` 仍返回 `1`。调用方**不得**用 `init` 的返回值决定是否调 `wait`。 + +### 1.4 测试环境 + +| 依赖 | 版本 / 说明 | +|---|---| +| 编译器 | 本机 MSVC(VS2019/2022/2026 任一),由 `run_unit_test.py` 自动探测 | +| Python | 仓库 `.venv` 或任意 python3(**纯标准库**) | +| 硬件 | 无(不需要开发板 / 串口 / ST-LINK) | + +工具链探测 / `INCLUDE`-`LIB` 拼接 / 编译运行 / 退出码判定全部委托通用 harness +[`../../test/host/common/unit_test_harness.py`](../../test/host/common/unit_test_harness.py), +本模块 `run_unit_test.py` 只**声明** `TestBuild`(源文件 / 桩目录 / 汇总行标记)。 + +### 1.5 运行方式 + +```powershell +# 仓库根目录(统一用 .venv) +& '.venv\Scripts\python.exe' test/host/vol_lock/run_unit_test.py +``` + +退出码:`0`=PASS / `1`=测试失败 / `2`=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +### 1.6 回归接入 + +| 项 | 说明 | +|---|---| +| **触发范围** | `App/fs/vol_lock.c` / `vol_lock.h`,或 `test/host/vol_lock/**`、`test/host/common/unit_test_harness.py` 变动 | +| **门禁层级** | **L0.5 层**(介于 L0 静态门禁与 L1 板端套件之间):主机侧、秒级、纯逻辑 | +| **pre-commit 钩子** | `vol-lock-unit-test`(本地钩子,`.pre-commit-config.yaml` §7f) | +| **汇总行** | `=== Volume Lock Tests: 33/33 PASSED (failed=0) ===` | +| **与板端测试的关系** | 互补:本单测保证**锁的调用契约**(fail-open / 幂等 / 透传);板端保证**真实的 FTL 并发不互踩**(dhara journal 竞争),后者需真机 + FTP 并发压测 | + +--- + +## 2. 测试用例 + +> 命名 `TC-VL-NNN`(VL = Volume Lock)。桩状态在每用例前由 `fakes_reset()` 复位到 +> "刚上电"(含把 `s_lock` 置 `NULL`)。断言数(33)多于用例数(14),因为每个用例 +> 通常包含 2~3 条子断言(后缀 `b` / `c`)。 + +### 2.1 `vol_lock_init` — 创建与幂等 + +| 用例 | 断言 | +|---|---| +| **TC-VL-001** | 首次调用返回 `0`;`osMutexNew` 恰好调用 1 次;模块静态态已持有句柄 | +| **TC-VL-002** | 重复调用仍返回 `0`(幂等);**不再**创建第二个互斥量(`osMutexNew` 仍为 1 次) | +| **TC-VL-003** | `osMutexNew` 返回 `NULL` 时 `init` 返回 `-1`;**不留下半初始化句柄**(`s_lock == NULL`) | +| **TC-VL-004** | 失败后重试可成功(返回 `0`);重试确实**再次**调用了 `osMutexNew` | + +### 2.2 `vol_lock_wait` — 获取与 fail-open ⭐ + +| 用例 | 断言 | +|---|---| +| **TC-VL-005** | 获取成功返回 `1`;`osMutexWait` 调用 1 次;**`timeout` 原样透传**(100 → 100) | +| **TC-VL-014** | `wait(0)` 的 try 语义:返回 `1` 且透传值为 `0`(defaultTask 路径) | +| **TC-VL-006** | `osMutexWait` 返回非 `osOK` → `wait` 返回 `0`;超时路径确实尝试过等待 | +| **TC-VL-007** | 锁未创建时 `wait` **自动 `init`** 并正常获取(创建 1 次 + 等待 1 次) | +| **TC-VL-008** ⭐ | **fail-open**:锁未创建且创建失败 → 返回 **`1`**;**`osMutexWait` 调用次数 == 0**(零阻塞);`s_lock` 仍为 `NULL` | +| **TC-VL-009** | fail-open 之后 `release` 无害:`osMutexRelease` **未被调用**(判空静默返回) | +| **TC-VL-010** | 已创建时多次 `wait` **不重复创建**(`osMutexNew` 恒 1 次);每次 `wait` 各等待一次 | + +> **TC-VL-008 是本套件存在的主要理由**:真机上需堆耗尽才可能触发,板端无法构造。 + +### 2.3 `vol_lock_release` — 释放与判空 + +| 用例 | 断言 | +|---|---| +| **TC-VL-011** | 已持锁时 `release` 调用 `osMutexRelease` **一次** | +| **TC-VL-012** | 锁未创建时 `release` **静默返回**(不调用、不崩溃) | +| **TC-VL-013** | 完整周期 `init → wait → release → wait → release`:全程只创建 **1** 个互斥量、释放 **2** 次、第二次 `wait` 的 `timeout` 透传正确(100) | + +**当前结果**:33/33 PASSED(`failed=0`)。 + +--- + +## 3. 如何扩展用例 + +1. 加一个 `static void tc_xxx(void)`;首行调 `fakes_reset()`; +2. 通过桩变量造场景:`s_new_fail = 1`(创建失败)、`s_wait_ok = 0`(等待超时); +3. `CHECK(cond, "描述")` 断言;在 `main()` 里注册(并同步更新本文档 §2 与变更记录)。 + +> ⚠ 用例之间**必须** `fakes_reset()`:`s_lock` 是全局态,不复位会把上一用例的句柄串进来。 + +--- + +## 4. 已知边界与设计取舍 + +### 4.1 白盒置 `s_lock = NULL` 是可接受的代价 + +`fakes_reset()` 直接写模块静态量 `s_lock`(源文件已被纳入同一编译单元,故可见)。 +这等价于"未初始化过",是主机侧白盒单测的常规手法。代价:若将来把 `s_lock` 改为 +`static const` 或移入结构体,`fakes_reset()` 需同步调整。 + +### 4.2 桩只验证"调用契约",不验证"互斥语义" + +桩的 `osMutexWait` 不模拟真实的互斥/阻塞/优先级继承行为,只记录**调用次数与参数**。 +因此本单测能保证的是: + +- ✅ 该调 `osMutexNew` 时调了、且只调一次(幂等); +- ✅ 该调 `osMutexWait` 时调了、`timeout` 值正确; +- ✅ **不该调时绝对没调**(fail-open 的零阻塞、判空 release)。 + +**不能**保证的是:真实互斥下的互斥性、优先级继承、超时精度——那些属 FreeRTOS 内核职责, +且板端 `FTP 并发压测`才是对应判据。 + +### 4.3 与板端的互补关系 + +| 层次 | 判据 | 手段 | +|---|---|---| +| 本单测(L0.5) | 锁的**调用契约**(fail-open / 幂等 / 透传 / 判空) | 主机侧桩注入 | +| 板端(L1) | 卷访问**真的串行**了(无 dhara journal 竞争、无 IWDG 误复位) | 真机 + FTP 与 syslog 并发压测 | + +`vol_lock.h` 记录的历史根因(2026-10-02:defaultTask 与 ftpTask 并发 FTL 写 → dhara journal +竞争 → 恢复旋转饥饿喂狗点 → IWDG 误复位)只能由板端判据复现;本单测负责**防止契约被改坏** +(例如有人把 fail-open 的 `return 1` 误改成 `return 0`,或给 fail-open 路径加上 +`osMutexWait`)。 + +--- + +## 5. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-10 | 创建;33 断言点全绿(`TC-VL-001..014`);覆盖 `init` 创建与幂等、`wait` 获取 / 超时 / 自动 init / **fail-open**、`release` 释放与判空;挂 pre-commit 钩子 `vol-lock-unit-test` | diff --git a/docs/测试说明/实机测试实施指南.md b/docs/测试说明/实机测试实施指南.md index 7ef87c33..20d91808 100644 --- a/docs/测试说明/实机测试实施指南.md +++ b/docs/测试说明/实机测试实施指南.md @@ -5,7 +5,7 @@ > **已完成事项**(S7、A1~A4)的实施细节与证据**不在本文**,见对应模块文档(§10 状态与去向)。 > **前置铁律**:本文所有步骤**均在接上 MCU / 串口 / ST-LINK 之后**执行; > 无实机时**只读本文,不执行任何烧录**。 -> **上游**:`错误码传播审计.md`(E-xx)· `待处理内容.md`(统一登记簿:P-x / N·S·T-x / +> **上游**:`错误码传播审计.md`(E-xx)· `docs/任务规划/README.md`(统一登记簿:P-x / N·S·T-x / > S1x·A-x / 阶段排序 / 诊断基线)· > `子系统设计/看门狗设计说明.md`(§7 验证记录)。 > **约定**:本文事项完成后**先销上游登记**,再在文末变更记录留一行; @@ -101,7 +101,7 @@ > **2026-10-03 台架结论**(实施细节与证据见 `子系统设计/看门狗设计说明.md` 变更记录): > V2/V4/V5 三用例全部通过——V2(任务饿死)声明 stall 后 ≤2.5s `reset cause: IWDG`;V4(关中断死循环)每 ~4.9s 稳定 IWDG 复位(IWDG 独立于中断);V5(30 min 真实负载)**120/120 次 FTP 传输全 PASS、零 IWDG 复位**。 -> **遗留观察**:V5 期间 44 次 stall 声明 100% 与 `e02_probe` 探针的 time_sync 30s 掉线+监听重建重合;隔离实验(仅 FTP 120s)**0 次 stall** → 探针场景专属,非一般负载误杀(已登记 `待处理内容.md`)。 +> **遗留观察**:V5 期间 44 次 stall 声明 100% 与 `e02_probe` 探针的 time_sync 30s 掉线+监听重建重合;隔离实验(仅 FTP 120s)**0 次 stall** → 探针场景专属,非一般负载误杀(已登记 `docs/任务规划/登记簿.md`)。 > 来源:`子系统设计/看门狗设计说明.md` §7 验证记录(原实施步骤已并入该文档文末变更记录)。 > 判定状态机与 IWDG 配置(PR/RLR 按设计值写入)已落地并经真机验证(2026-10-02,`930215f`, @@ -113,7 +113,7 @@ |---|---|---|---| | **V2** | 任务饿死 | 临时在 `netTask` 循环插 `while(1);` | 800 ms 后串口 `[ERR] WATCHDOG: task stall mask=0x...`;≤2.5 s 复位;重启后 `[WD] reset cause=watchdog reset` | | **V4** | 关中断死循环 | 临时插 `taskDISABLE_INTERRUPTS(); while(1);` | ≤2.5 s 复位(检验 IWDG **独立于中断**);`cause=watchdog reset` | -| **V5** | 长期不误杀(**核心验收项**) | FTP 大文件传输 + UI 菜单遍历 + 配置保存 + 网络重配 **≥30 min** | **零复位**(重点验证 §4.4 白名单是否完备) | +| **V5** | 长期不误杀(**核心验收项**) | FTP 大文件传输 + UI 菜单遍历 + 配置保存 + 网络重配 **≥30 min** | **零复位**(重点验证 `docs/子系统设计/看门狗设计说明.md` §4.3 白名单是否完备) | > ⚠️ **V5 是核心验收项**:误杀比不杀更有害。必须在**真实业务负载**下跑足时长。 @@ -124,7 +124,7 @@ # 1) 基线:跑一轮产品 @test 确认无复位 & '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py -# 2) 注入:按设计说明 §10 在 netTask 循环插 while(1);(或 taskDISABLE_INTERRUPTS(); while(1);) +# 2) 注入:按 `docs/子系统设计/看门狗设计说明.md` §7 在 netTask 循环插 while(1);(或 taskDISABLE_INTERRUPTS(); while(1);) # —— 改完 @build,然后烧录 & '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py & '.venv\Scripts\python.exe' .dsh\skills\stlink-flash\scripts\stlink_flash.py flash MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex @@ -148,7 +148,7 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L **风险/回滚**: - 故障注入用例**会复位设备多次**——确认设备非在用/非已部署。 - 长跑涉及 NAND 写入(FTP 上传)**消耗 NAND 块**——若设备已接近容量上限,改用只读传输。 -- ⚠️ **R5 风险**(设计说明 §9):NAND 擦写偶发超 800 ms 可能误复位——若 V5 出现偶发复位, +- ⚠️ **R5 风险**(`docs/子系统设计/看门狗设计说明.md` §9):NAND 擦写偶发超 800 ms 可能误复位——若 V5 出现偶发复位, 先按设计说明增大容忍次数或把存储操作改分段置位,**不要盲目放宽全局超时**。 --- @@ -191,7 +191,7 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L > **顺带修复(编排器缺陷,非本次代码引入)**:`run_all_suites.py` 原对所有套件统一 `--forbidden "HardFault;assert;[ERR]"`,而 GD5F 的 `TC-GD5F-704`(CCM 非 DMA 可达缓冲)会**预期**打出 `[ERR][NAND] RX DMA buffer not DMA-reachable ... fallback poll`(用例 PASS)→ GD5F 套件**永远被判 FAIL**(自 `2055de0` 起)。按 `verify_log.py` 的说明改为**按套件覆盖 `--forbidden`**(GD5F = `HardFault;assert`),修复后 PASS。 > **来源**:`错误码传播审计.md` 第二档 #1(E-10/E-42/E-53/E-54/E-56/E-57); -> `待处理内容.md` §5.2 四要素骨架(设计基线)。 +> `docs/任务规划/基线与待确认.md` §2.2 四要素骨架(设计基线)。 **现状证据**:`ch395f.c`(1330 行) 与 `gd5f2gq5ue.c`(1496 行) 是同代驱动,但**只有 CH395F** 形成了"分类 + 传播 + 审计"完整体系;GD5F 错误处理散落各函数,`Trap_Records` 仅 198 行。 @@ -249,7 +249,7 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L - **正常路径 ✅**:V5 长跑(30 min,FTP 512 KB 上/下传)**120/120 PASS、零 IWDG 复位**, 网络重建后的 FTP/time_sync 服务据代次号重建监听、功能正常(证据 `/tmp/v5_soak_serial_20261003.log`)。 - **负向路径(拔线/芯片级 `net_init` 失败)**受**台架拓扑**限制(CH395↔交换机, - 拔交换机侧线缆链路仍 UP;芯片级失败需 SPI 故障注入)→ 已登记 `待处理内容.md` **N-16** + 拔交换机侧线缆链路仍 UP;芯片级失败需 SPI 故障注入)→ 已登记 `docs/任务规划/登记簿.md` **N-16** (① PHY 拔线负向 ② A2 `net_init` 失败分支 ③ A4 短写压测),需专项窗口。 ### 7.2 N-2:49s 全系统日志静默复现与定位 · ✅ 判据②已满足 / 判据①未复现 @@ -424,7 +424,7 @@ CH395F / STORAGE / GD5F,未含 RTC;怀疑 RTC 构建下 `testTask` 不被创 | 2026-10-02 台架取证链(IWDG PR/RLR 错配 / dhara resume 回滚 / ORE 僵尸通道) | ✅ 已修 | `子系统设计/看门狗设计说明.md` 2026-10-02 变更记录 · `架构设计/STM32F4-Base存储架构说明.md` §5.4.1 · `子系统设计/控制台单写者日志设计说明.md` §12;提交 `930215f` | > **真机判定汇总**(2026-10-03,本机 MCU,新固件全闭环 `@test` PASS 7/7):A1 ✅ PASS(含配置持久化); -> A2/A3/A4 正常路径零回归;芯片级失败分支 / 短写 / 拔线负向场景记 `待处理内容.md` N-16,待专项台架窗口。 +> A2/A3/A4 正常路径零回归;芯片级失败分支 / 短写 / 拔线负向场景记 `docs/任务规划/登记簿.md` N-16,待专项台架窗口。 --- @@ -436,7 +436,7 @@ CH395F / STORAGE / GD5F,未含 RTC;怀疑 RTC 构建下 `testTask` 不被创 | V2/V4/V5 | `子系统设计/看门狗设计说明.md` §7 验证记录 | `子系统设计/看门狗设计说明.md` 变更记录(2026-10-03) | | S11 堆告警 | —(原复审项,已闭环 2026-10-03) | `内存管理/RAM使用分析与优化方案.md`(基线更新) | | S9 GD5F | `问题记录/错误码传播审计.md` 第二档 #1 | `问题记录/GD5F2GQ5UE_Trap_Records.md`、`测试说明/GD5F2GQ5UE_Test_Guide.md` | -| N-2 日志静默 | `待处理内容.md` N-2 | `问题记录/错误码传播审计.md` E-02 | +| N-2 日志静默 | `docs/任务规划/登记簿.md` N-2 | `问题记录/错误码传播审计.md` E-02 | | P1-8 网络重建 | —(原台账项,已闭环 2026-10-02) | `网络/Net层阻塞模型架构设计.md` | | S5/S4 | —(原复审项,已闭环 2026-10-03) | `测试说明/测试开发标准流程.md` | | 模拟器差距 | `测试说明/模拟器差距清单.md` §3 决策规则 | 本文为"需回真机"项的执行面 | diff --git a/docs/测试说明/时间同步单元测试说明.md b/docs/测试说明/时间同步单元测试说明.md new file mode 100644 index 00000000..c5fd3180 --- /dev/null +++ b/docs/测试说明/时间同步单元测试说明.md @@ -0,0 +1,239 @@ +# 时间同步单元测试说明(主机侧) + +> **模块**:Time Sync(`App/sync/time_sync.c` + `App/sync/time_sync.h`) +> **测试类型**:**主机侧纯逻辑单元测试**(L0.5 层)——在 PC 上离线验证 TCP 时间同步协议 +> 状态机(分片喂狗延时 / 监听建立与退避 / 时间包重组 / 任务主循环),零硬件、零烧录、 +> 毫秒级、不占 MCU 的 RAM。 +> **状态**:✅ 已落地(80 断言点,`/W4` 0 告警,已挂 pre-commit 钩子) +> **日期**:2026-10-10 +> **关联文档**: +> [`../../AGENTS.md`](../../AGENTS.md)(平台约束与看门狗铁律)、 +> [`测试开发标准流程.md`](测试开发标准流程.md)(测试分层与门禁总览)、 +> [`看门狗单元测试说明.md`](看门狗单元测试说明.md)(喂狗位图判定状态机)、 +> [`卷锁单元测试说明.md`](卷锁单元测试说明.md)(同批落地的姊妹篇)、 +> [`../子系统设计/看门狗设计说明.md`](../子系统设计/看门狗设计说明.md)(`required_mask` / 抽样窗 / 长耗时白名单)、 +> [`../任务规划/README.md`](../任务规划/README.md)(**N-17** 登记条目)、 +> [`../问题记录/错误码传播审计.md`](../问题记录/错误码传播审计.md)(**E-02** 头阻塞) + +--- + +## 1. 概述 + +### 1.1 为什么单独做主机侧单测 + +本模块是**两件已知风险的当事方**,而两件风险的成因**全部是本模块的纯逻辑**: + +1. **N-17(看门狗余量)**:登记簿 N-17 记录"time_sync 监听重建期间心跳缺口 800ms~1s, + 距 IWDG 最坏超时 1701ms 余量仅约 0.7s"。该缺口的每一段都由本模块产生—— + `time_sync_delay_fed()` 的**分片喂狗节奏**、监听重试退避(500 / 2000ms)、 + `accept` 轮询退避(200ms)、收尾延时(50ms),以及"就绪屏障等待态也要喂心跳"。 + 真机上只能靠"跑起来看复位不复位"间接观察;主机侧可**逐片断言**心跳次数与片长。 +2. **E-02(头阻塞)**:旧实现用单次 30s 阻塞 `net_recv` 等时间包,独占 `netTask`, + 使其余任务的网络操作整体停摆。现改为"分片有界读 + 总预算",本单测钉死其**分片语义** + (单次等待片长恒 ≤ 50ms、总预算 30s 不因分片而改变)。 + +次要理由:**时间包重组状态机**(累积到 8 字节才解析、TCP 分段送达、EOF / 真错误 / +本片超时三种返回码分支、tick 回绕)是纯逻辑,板端套件无法精确构造 +"分段 + 预算耗尽 + tick 回绕"这类组合。 + +### 1.2 被测对象与桩 + +测试单元**原样 `#include` 生产源文件**,平台依赖由 7 个桩头替身 + 1 个真实头组成: + +``` +test/host/time_sync/time_sync_test.c + ├── #include "FreeRTOS.h" / "task.h" / "cmsis_os2.h" ← 桩 + ├── #include "net_socket.h" / "sys_clock.h" / + │ "sys_events.h" / "dbg_log.h" ← 桩 + ├── #include "task_watchdog.h" ← 真实头(App/util/) + └── #include "time_sync.c" ← 生产源文件(App/sync/),原样纳入 +``` + +| 头文件 | 来源 | 桩提供的能力 | +|---|---|---| +| `FreeRTOS.h` | 桩 | `TickType_t` = `uint32_t`;`configTICK_RATE_HZ` = 1000(故 `pdMS_TO_TICKS(ms) == ms`) | +| `task.h` | 桩 | `vTaskDelay`(记录每片时长)、`xTaskGetTickCount`(返回可控 tick) | +| `cmsis_os2.h` | 桩 | `osEventFlagsWait`(按脚本返回就绪 / 超时) | +| `sys_events.h` | 桩 | `sys_evtHandle` + `SYS_EVT_NET_READY` 等位定义 | +| `sys_clock.h` | 桩 | `sys_clock_set_unix`(记录 epoch 与调用次数) | +| `dbg_log.h` | 桩 | `DBG_ERROR` / `DBG_INFO` 计数(`g_tstest_err_count` / `g_tstest_info_count`) | +| `net_socket.h` | 桩 | `socket` / `bind` / `listen` / `accept` / `close` / `recv_to` / `get_generation` / `htons` **全部脚本化** | +| `task_watchdog.h` | **真实**(`App/util/`) | 提供 `task_wd_id_t` / `TASK_WD_TIMESYNC`;`task_wd_beat` / `task_wd_bind` 由测试文件实现(记录心跳节奏与位号) | +| `time_sync.h` | **真实**(`App/sync/`) | 只 `#include `,无平台依赖 | + +`include_dirs` 顺序为 **`stub` → `App/util` → `App/sync` → 本目录**:桩目录必须排在 +前面,否则会被真实头抢先命中。 + +> ⚠ `dbg_log.h` 桩**不得**定义 `DBG_LINE_BUF_MAX` / `DBG_TAG`——生产源(`time_sync.c:28-29`) +> 已先定义它们,桩再定义会触发 C4005 宏重定义告警。 + +### 1.3 关键约定:分片喂心跳与报文重组契约 + +**(一)心跳必须落在同一个 100ms 抽样窗内。** +看门狗抽样窗为 `WD_POLL_PERIOD_MS = 100ms`(`App/util/task_watchdog.c`)。若一次长等待 +(如 2000ms 退避)期间不喂心跳,或心跳节奏恰好滑过两个连续抽样窗,该任务就会被判"卡死"。 +故 `time_sync_delay_fed()` 把任意长等待切成 **≤ `TIME_SYNC_WAIT_SLICE_MS` = 50ms** 的短片, +**每片前**喂一次心跳——语义等价于一次长延时,仅把心跳节奏钉在窗内(`round 8b` 消抖)。 + +**(二)`net_recv_to()` 的四态返回。** +`time_sync_handle_conn()` 的重组循环依赖该契约: + +| 返回 | 含义 | 本模块动作 | +|---|---|---| +| `> 0` | 收到 `n` 字节 | 累加;`>= 8` 即收齐并解析,否则继续等余下字节 | +| `0` | 对端关闭(EOF) | 退出循环,**不解析** | +| `-1` | 真错误 | 退出循环,**不解析** | +| `-2` | 本片预算等满、无数据 | 重算剩余预算,**下一片**继续 | + +**(三)总预算不因分片而改变。** +单次 `net_recv_to` 的等待片长 ≤ 50ms,但**跨片累积**仍以 `t0 = xTaskGetTickCount()` +为基准、总预算 `TIME_SYNC_TOTAL_BUDGET_MS` = 30000ms。tick 回绕由无符号减法天然处理。 + +### 1.4 测试环境 + +| 依赖 | 版本 / 说明 | +|---|---| +| 编译器 | 本机 MSVC(VS2019/2022/2026 任一),由 `run_unit_test.py` 自动探测 | +| Python | 仓库 `.venv` 或任意 python3(**纯标准库**) | +| 硬件 | 无(不需要开发板 / 串口 / ST-LINK / 对端 PC) | + +工具链探测 / 编译运行 / 退出码判定全部委托通用 harness +[`../../test/host/common/unit_test_harness.py`](../../test/host/common/unit_test_harness.py), +本模块 `run_unit_test.py` 只**声明** `TestBuild`。 + +### 1.5 运行方式 + +```powershell +# 仓库根目录(统一用 .venv) +& '.venv\Scripts\python.exe' test/host/time_sync/run_unit_test.py +``` + +退出码:`0`=PASS / `1`=测试失败 / `2`=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +### 1.6 回归接入 + +| 项 | 说明 | +|---|---| +| **触发范围** | `App/sync/time_sync.c` / `time_sync.h`、`App/util/task_watchdog.h`,或 `test/host/time_sync/**`、`test/host/common/unit_test_harness.py` 变动 | +| **门禁层级** | **L0.5 层**(介于 L0 静态门禁与 L1 板端套件之间):主机侧、秒级、纯逻辑 | +| **pre-commit 钩子** | `time-sync-unit-test`(本地钩子,`.pre-commit-config.yaml` §7g) | +| **汇总行** | `=== Time Sync Tests: 80/80 PASSED (failed=0) ===` | +| **与板端测试的关系** | 互补:本单测保证**延时节奏与重组逻辑**符合设计;板端保证**真实 TCP 时序 + IWDG 不误复位**(N-17 的实际余量只能由板端长跑判定) | + +--- + +## 2. 测试用例 + +> 命名 `TC-TS-NNN`(TS = Time Sync)。断言数(80)多于用例数(27),因为每个用例通常 +> 包含 2~9 条子断言(后缀 `b` / `c` …)。桩状态在每用例前由 `fakes_reset()` 复位。 + +### 2.1 `time_sync_delay_fed` — 分片喂心跳延时(TC-TS-001..006) + +| 用例 | 断言 | +|---|---| +| **TC-TS-001** | `delay_fed(0)` 既不睡眠也不喂狗(零开销快路径) | +| **TC-TS-002** | 恰好一片(50ms):1 次 `vTaskDelay(50)` + 1 次心跳 | +| **TC-TS-003** | 非整片(120ms)切成 **50 / 50 / 20** 三片,每片喂一次心跳 | +| **TC-TS-004** | 长延时(2000ms)= **40 片 × 50ms**,每片喂心跳(监听退避路径) | +| **TC-TS-005** | 不足一片(49ms)单片处理,**不**被拆成 0 | +| **TC-TS-006** | 总预算 30s = **600 片**,片长上限**恒** 50ms | + +### 2.2 `time_sync_open_listener` — 监听建立与退避(TC-TS-010..015) + +| 用例 | 断言 | +|---|---| +| **TC-TS-010** | 首次成功:`socket` / `bind` / `listen` 各 1 次;端口经 `net_htons(8888)`;`sin_family` = `NET_AF_INET`;成功路径**无退避延时** | +| **TC-TS-011** | `bind` 失败 → **关闭该 socket** + 退避 2000ms(40 片)+ 留痕 `DBG_ERROR`,随后重试成功 | +| **TC-TS-012** | `listen` 失败 → 关闭该 socket + 退避 2000ms,随后重试成功 | +| **TC-TS-013** | `socket` 连续失败 → **5 次后返回 -1**;5 条 `DBG_ERROR`;5 × 500ms 退避(50 片) | +| **TC-TS-014** | 第 3 次重试成功并返回**新**描述符;恰好尝试 3 次;两次失败退避 500ms × 2 | +| **TC-TS-015** | 心跳总次数 = 每轮重试(5)+ 每片延时(50);**心跳位恒为 `TASK_WD_TIMESYNC`** | + +### 2.3 `time_sync_handle_conn` — 时间包重组状态机(TC-TS-020..029) + +| 用例 | 断言 | +|---|---| +| **TC-TS-020** | 8 字节一次收齐 → **大端**解析 epoch + 更新墙钟 1 次 + 关连接;首次请求 16 字节;单次等待片长 = 50ms;留痕 `wall clock updated` | +| **TC-TS-021** | 分段送达(4+4)→ 累积后解析正确;读了两次;第二次**只请求剩余 12 字节**;每片 50ms | +| **TC-TS-022** | 一次到达 16 字节 → **只取前 8 字节**;一次读够即停 | +| **TC-TS-023** | 前缀非 `"TIME"` → **不更新墙钟**,但仍关闭连接 | +| **TC-TS-024** | 对端关闭(`n == 0`)→ 立即退出、不解析;关闭连接 | +| **TC-TS-025** | 真错误(`n == -1`)→ 立即退出、不解析 | +| **TC-TS-026** | 只收到 4 字节后 EOF → **不足 8 字节不解析**;期间继续等待后续字节 | +| **TC-TS-027** | 30s 预算耗尽 → 不再读(只读 1 次)+ 留痕 `no TIME packet in 30s, drop conn` + 关连接 | +| **TC-TS-028** | 剩余预算不足一片 → **末片按剩余(20ms)切分** | +| **TC-TS-029** | **32 位 tick 回绕** → 预算计算仍正确(无符号减法) | + +### 2.4 `time_sync_task` — 任务主循环(TC-TS-040..044) + +| 用例 | 断言 | +|---|---| +| **TC-TS-040** | `sys_evtHandle == NULL` → **跳过就绪屏障**;`accept` 轮询 3 次(2 次无连接 + 1 次成功);收到时间包并更新墙钟;延时片数 = 4+4+1(200+200+50ms)且**全部按 50ms 分片**;心跳次数 = 主循环 1 + 退避 8 + 收连接 1 + 收尾 1 + 次轮 1 = **13**;所有心跳均打 `TASK_WD_TIMESYNC` 位 | +| **TC-TS-041** | 就绪屏障轮询 3 次(2 次未就绪 + 1 次就绪);屏障等待按 **100ms 分片**;等待 `SYS_EVT_NET_READY` 位;`osFlagsWaitAny \| osFlagsNoClear`(**就绪位不被消费**);未就绪的两次各喂一次心跳;仍先 `task_wd_bind` 登记心跳路由 | +| **TC-TS-042** | 屏障等待 4 次均未就绪 → **循环未挂死**(跳出并继续);每次未就绪后各喂一次心跳(2026-10-02 台架根因的**回归防线**) | +| **TC-TS-043** | 网络代次变化 → **关闭旧监听**并重监听;留痕 `network re-init, relisten`(另含 2 条 listen 成功);每次循环只轮询一次 `accept` 即放弃 | +| **TC-TS-044** | 监听建立失败 → 内部重试 5 次后放弃;6 条 `DBG_ERROR`(5 条 `net_socket FAIL` + 1 条 `retry later`);退避 5×500ms + 2000ms = **90 片**;心跳次数 = 2 + 5 + 50 + 40 = **97**;所有心跳均打 `TIMESYNC` 位 | + +**当前结果**:80/80 PASSED(`failed=0`)。 + +--- + +## 3. 如何扩展用例 + +1. **纯延时类**:直接调 `time_sync_delay_fed(ms)`,断言 `s_delay_calls`(片数)与 + `s_delays[]`(每片时长)以及 `s_beat_calls`(心跳数)。 +2. **脚本化网络**:给 `s_gen_seq[]` / `s_evt_seq[]` / `recv_step_t` 数组填序列, + 再驱动被测函数。 +3. **跳出任务循环**:生产 `time_sync_task` 是 `for(;;)`,无法自然返回。桩在脚本耗尽时 + 调 **`longjmp(s_jmp, N)`** 强行跳出(见 §4.1),跳出后正常断言。 +4. `CHECK(cond, "描述")` 断言;在 `main()` 里注册(并同步更新本文档 §2 与变更记录)。 + +--- + +## 4. 已知边界与设计取舍 + +### 4.1 用 `setjmp` / `longjmp` 跳出 `for(;;)` + +`time_sync_task` 是永不返回的任务循环。本单测在 `main()` 里 `setjmp` 设跳转点, +桩在"脚本化调用计数达到预设值"时 `longjmp` 回来,然后断言循环期间产生的副作用 +(心跳次数 / 延时片数 / `accept` 次数 / 墙钟更新)。 + +**取舍**:这比"把循环体抽成可测函数"更贴近真实——**测的就是任务主体本身**, +不需要为了可测性改动生产代码结构。代价是**必须精确设定跳出点**:跳出点设错 +(多一次或少一次桩调用)会导致断言数不符,`TC-TS-040` 的跳出点曾因此修正。 + +### 4.2 ⚠ C 的"暂定定义"会静默合并同名静态量 + +本文件曾出现一个**无任何编译告警**的缺陷:文件作用域下两处 `static int s_bind_calls;` +(一处给 `task_wd_bind` 桩、一处给 `net_bind` 桩)被 C 的**暂定定义(tentative +definition)规则静默合并为同一个对象**——于是 `task_wd_bind` 与 `net_bind` 共用一个计数器, +8 条断言失败(72/80)。 + +**教训**:同一编译单元内**所有** `static` 变量名必须唯一,哪怕是不同桩的"各自的"计数器。 +修复手法是把看门狗侧改名为 `s_wd_bind_calls` / `s_wd_bind_id_bad`,并加注释说明。 +扩展用例时**务必**先 `grep` 目标名字是否已被占用。 + +### 4.3 桩不模拟真实 TCP 时序 + +`net_recv_to` / `net_accept` 桩按脚本返回,**不**模拟真实的分段到达时刻、重传、窗口、 +CH395F 缓冲区边界。因此本单测能保证的是"**给定返回码序列,状态机反应正确**", +**不能**保证真实链路上的时序行为——后者属板端与 PC 对端(`test/host/time_sync_server.py`)职责。 + +### 4.4 N-17 的实际余量仍由板端判定 + +本单测把"每片喂心跳、片长 ≤ 50ms、退避值正确"这些**逻辑前提**钉死,但 N-17 关注的 +是**端到端心跳缺口 vs IWDG 超时**——这还取决于真实调度延迟、`netTask` 排队时长、 +Flash 写耗时等主机侧无法建模的因素。故: + +- 本单测 ✅ 保证"逻辑上不会出现超过 50ms 的**无喂狗**窗口"; +- 板端 ✅ 保证"实测缺口 + 调度抖动仍小于 1701ms"。 + +两者都过,N-17 才能闭环。 + +--- + +## 5. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-10 | 创建;80 断言点全绿(`TC-TS-001..006` / `010..015` / `020..029` / `040..044`);覆盖分片喂狗延时、监听建立与退避、时间包重组(半包 / 粘连 / EOF / 真错 / 预算耗尽 / tick 回绕)、任务主循环(就绪屏障 / accept 轮询 / 代次变化重听 / 监听失败退避);修复静态量暂定定义合并缺陷;挂 pre-commit 钩子 `time-sync-unit-test` | diff --git a/docs/测试说明/模拟器差距清单.md b/docs/测试说明/模拟器差距清单.md index 696a9541..ea85b38e 100644 --- a/docs/测试说明/模拟器差距清单.md +++ b/docs/测试说明/模拟器差距清单.md @@ -3,7 +3,7 @@ > **用途**:把散落在各子系统文档里的"模拟器无法建模 X"汇成**单一事实源**,回答一个关键问题—— > **模拟器回归通过 ≠ 真机通过,那么差在哪?差多少?哪些必须真机补测?** > **性质**:**登记册**(living document)。新增模拟器后端差异时必须在此登记一行,不得只写在别处。 -> **上游依据**:`待处理内容.md` §5.1(N-4 二期门禁)。 +> **上游依据**:`docs/任务规划/基线与待确认.md` §2.1(N-4 二期门禁)。 > **相关**:`UI设计/LVGL模拟器部署与使用说明.md`(部署与使用)、`simulator/README.md`(目录与架构)、 > `测试说明/测试开发标准流程.md`(L3 门禁在测试分层中的位置)。 @@ -51,7 +51,7 @@ | # | 差距 | 模拟器行为 | 真机行为 | 影响 | 回归补测 | |---|---|---|---|---|---| -| L-1 | **控制台单写者无对应实现** | `simulator/dbg_log.h` 把 `DBG_*` 映射到 `printf`;**不编译** `console_log.c` | 单写者环形缓冲 + defaultTask 唯一发送 | `console_log` 的环形缓冲溢出/丢弃计数逻辑**模拟器测不到**;共享代码若误调 `console_log.h` 声明,模拟器链接失败(已被此约束捕获) | `@test` + 板端串口观测;`docs/子系统设计/控制台单写者日志设计说明.md` §219 | +| L-1 | **控制台单写者无对应实现** | `simulator/dbg_log.h` 把 `DBG_*` 映射到 `printf`;**不编译** `console_log.c` | 单写者环形缓冲 + defaultTask 唯一发送 | `console_log` 的环形缓冲溢出/丢弃计数逻辑**模拟器测不到**;共享代码若误调 `console_log.h` 声明,模拟器链接失败(已被此约束捕获) | `@test` + 板端串口观测;`docs/子系统设计/控制台单写者日志设计说明.md` §4.1 | | L-2 | **mtime 粒度不同** | 宿主文件 mtime(**秒级**) | FAT 打包时间(`fdate<<16\|ftime`,**分钟级**) | `cfg_file_poll` 的 `(size, mtime)` 变更令牌在两端触发时机不同;分钟级粒度下的"同分钟内两次改动"在真机可能被判为无变化 | 板端 STORAGE 套件;`配置文件设计说明.md` §变更检测 | | L-3 | **无应用回调(空操作)** | 模拟器不注册段回调,`cfg_file_poll` 重载后回调为空 | 各段回调真正执行(如网络重配置) | "配置变更 → 触发回调 → 副作用"链路在模拟器上**只验证到写盘**,回调逻辑不覆盖 | `@test`(配置保存 → UI 生效) | | L-4 | **堆后端不同** | `cfg_store_alloc` = 宿主 `malloc`(堆余量以 MB 计) | FreeRTOS 堆(`configTOTAL_HEAP_SIZE` 48 KB,实测最低水位 ~4.8 KB) | **分配失败路径在模拟器上永不触发**——P0-1 类"KB 级分配必然返回 NULL"缺陷模拟器照不出来(`AGENTS.md` 已列为铁律) | 内存预算门禁 + 真机 `[HEAP]` 水位告警(S11) | @@ -66,7 +66,7 @@ | H-4 | **SD2506 RTC** | 无 | I2C RTC:时间读写、闹钟、温度、电池电压 | 时间源相关(`sys_clock_init` 降级路径)在模拟器上不可触发 | 板端 RTC 套件(18 断言点)+ `sys_clock` L0.5(已覆盖算术面) | | H-5 | **TPAFE5160 ADC** | 无 | SPI ADC + BUSY EXTI 中断 | 中断驱动采集链路 | 待功能实现(上游 B1) | | H-6 | **RS-485 半双工** | 无 | UART4 + DE 方向控制,DWT 微秒延时 | DE 收发切换时序(特有陷阱) | 待功能实现(上游 B2) | -| H-7 | **启动段硬件初始化** | 只跑纯软件部分 | I2C/SPI 外设初始化、BBT 扫描耗时、FatFS 挂载 | 启动段死机(无看门狗保护窗口)**只能在真机复现** | 真机开机日志判读(`项目启动顺序管理.md` §6.8,S10 落地) | +| H-7 | **启动段硬件初始化** | 只跑纯软件部分 | I2C/SPI 外设初始化、BBT 扫描耗时、FatFS 挂载 | 启动段死机(无看门狗保护窗口)**只能在真机复现** | 真机开机日志判读(`项目启动顺序管理.md` §6,S10 落地) | ### 2.4 哪些差异是"有意的对齐"(非缺陷) diff --git a/docs/测试说明/测试开发标准流程.md b/docs/测试说明/测试开发标准流程.md index 8dcb9ada..56bcf6b7 100644 --- a/docs/测试说明/测试开发标准流程.md +++ b/docs/测试说明/测试开发标准流程.md @@ -329,7 +329,7 @@ static void phaseN_run(void) { | `App/log/sys_log_mcu.c` | — | ⬜ 候选 | 记录编解码(定长 64B) | 编解码纯逻辑;写入端依赖 FatFS + 队列,测编解码即可 | | `App/util/time_sync.c` | — | ⬜ 候选 | 协议帧编解码 | 报文组装/解析纯逻辑;网络收发不测 | | `Drivers/BSP/NETCFG/net_cfg_legacy.c` | — | ⛔ 已删除(2026-10-10) | — | 旧档 NCFG 迁移整体移除(旧档 `/cfg/netcfg.bin` 不读、不删、不迁移),测试候选随之撤销 | -| `App/net/*`(网络栈上层) | — | ⬜ 待评估 | 依赖 socket/CH395F | 纯协议状态机部分或可测,需先确认抽象质量 | +| `App/protocol/iec104/*`(104 协议栈) | — | ✅ 已覆盖(82 断言,2026-10-12 B4) | 数据源 vtable + tick 注入 + 发送回调注入 | APCI 重组/总召分片/COV/k-w-t1~t3/两段式全离线穷举;`iec104-unit-test` 钩子 | | `App/util/rs485.c` | — | ⏸ 待功能实现 | — | 上游 B2;当前为空桩 | | `Drivers/BSP/TPAFE5160/*.c` | — | ❌ 不适格 | — | 寄存器序列为主,需真机时序 | | 其余驱动(CH395F/GD5F2GQ5/SD2506) | — | ❌ 不适格 | — | 需硬件时序 / SPI / I2C 总线 | @@ -529,7 +529,7 @@ flowchart LR > 新增任务/栈/缓冲/代码导致的体积增长**不会在 CI 被拦**。故该跳过以 `::warning::` 明示 > (不是弱化的 notice)并写入 step-summary。**兜底必须靠本地**:改代码后 `@build`(0/0) > → `python scripts/check_mem_numbers.py --scan`。详见 -> [`../环境与部署/CI配置指南.md`](../环境与部署/CI配置指南.md) §0.0。 +> [`../环境与部署/CI配置指南.md`](../环境与部署/CI配置指南.md) §0。 ### 9.2 两个 job @@ -619,10 +619,10 @@ CI 只在 push / PR 时跑,且需 runner 就绪。**提交前自查、或 runn | 2026-10-02 | **S6**:`test/run_all_suites.py` 新增 **`--check-build`**(只编译不烧录,纯离机,不需要 MCU/串口/ST-LINK);新增 **§3.6.1** 说明其解决的问题(测试构建无独立门禁,2026-08~09 CH395F 套件实例)、判定语义与用法。**实测 4 套件全部 `BUILD_OK: 0 errors, 0 warnings`**(即 T-4 提到的"CH395F 不可编译"已修复) | | 2026-10-02 | **S1-b + S2**:新增 **§4.2.1「L0.5 适格模块台账」**(判据 + 模块状态表 + "两条实证":sys_clock / cfg_file 首测即各抓一处生产缺陷);§4.1 登记 `test/host/cfg_file/` 新单测。同步新增 [`配置文件单元测试说明.md`](配置文件单元测试说明.md)(含抓出的 `s_encode` 返回值误判 P0 缺陷记录) | | 2026-10-02 | **S3 + S14**:§9.1 第 2 条改正(原文"强制跑 `mem-numbers`"自相矛盾 → 改为"按 `.map` 有无自适应跳过")并加 🔴 门禁盲区声明;§9.2 补 S3 结果可见性(`Tee-Object` 落盘 + `$GITHUB_STEP_SUMMARY` + `::notice::` 汇总行,不用 `actions/upload-artifact`)。另:§4.1 登记 `test/host/sys_clock/` 新单测(S1-a) | -| 2026-10-01 | 新增 **§9 CI 持续集成(Gitea Actions,T-6)**:`.gitea/workflows/ci.yml`(`l0-gates` + `l05-host-unit-tests`,Windows runner);**L0 + L0.5 上 CI、L1/L2/L3 留本地**;刻意不跑 `@build`、`.map` 缺失时自适应跳过 `mem-numbers`;新增 `scripts/ci_local.py` 本地等价命令(§4.1 登记);§1 补 CI 说明;`docs/待处理内容.md` T-6 销项 | +| 2026-10-01 | 新增 **§9 CI 持续集成(Gitea Actions,T-6)**:`.gitea/workflows/ci.yml`(`l0-gates` + `l05-host-unit-tests`,Windows runner);**L0 + L0.5 上 CI、L1/L2/L3 留本地**;刻意不跑 `@build`、`.map` 缺失时自适应跳过 `mem-numbers`;新增 `scripts/ci_local.py` 本地等价命令(§4.1 登记);§1 补 CI 说明;`docs/任务规划/README.md` T-6 销项 | | 2026-10-01 | §3.6 新增 **`test/run_all_suites.py`** 多套件回归编排(`try/finally` 无条件还原套件开关;T-5);`test/README.md` 同步 | | 2026-10-01 | §3.2 断言宏改为**集中内核**:新增 `test/board/test_common.h`(`test_stats_t` + `TEST_CHECK_IMPL`/`TEST_REPORT_IMPL`),4 套件各自用一行宏绑定自身统计变量(T-4;零行为变化、零 RAM 增量、`@build` 0/0,四套件均 0/0 验证)。附 `test_common.h` 不 include `dbg_log.h` 的原因(`DBG_TAG` 冻结 → `#47-D`) | -| 2026-10-01 | 新增 §7.1 钩子 **`test-suite-switch`**(测试套件开关=产品构建,防测试固件静默提交)+ §3.1 铁律(跑完套件回归务必改回产品构建);依据 `docs/待处理内容.md` T-1(测试架构评审 P0)。同步更新 `../代码规范/pre-commit使用说明.md` §2.13 | +| 2026-10-01 | 新增 §7.1 钩子 **`test-suite-switch`**(测试套件开关=产品构建,防测试固件静默提交)+ §3.1 铁律(跑完套件回归务必改回产品构建);依据 `docs/任务规划/README.md` T-1(测试架构评审 P0)。同步更新 `../代码规范/pre-commit使用说明.md` §2.13 | | 2026-10-01 | §4.1/§4.2 更新:主机侧单测运行器统一走通用 harness `test/host/common/unit_test_harness.py`(T-3),模块运行器只做声明式配置;§6.2.1 新增套件汇总行校验(T-2) | | 2026-10-01 | §4.2 补 **L0.5 环境要求表**(MSVC `cl.exe` = 装了 VS 即有、与模拟器共用;Windows SDK;纯标准库 Python;缺失时退出码 2 且 pre-commit 钩子拦截)+ "不调 `vcvars64.bat`(`reg.exe` 受限)"原因说明;§4.1 工具清单标注该运行器需 VS。同步更新 `../环境与部署/项目部署环境要求.md`(§2.3 / §2.7 / 第 3、10 步 / 清单 / 下载表 / 速查表) | | 2026-09-15 | §3.4 更新:P1-6 修复落地(基础线程策略)——USER CODE 区冗余 netTask 创建删除 + `net_task_func` 入口 `ENABLE_HW_LAYER_TESTS` 自挂起守卫;"代码事实/设计约束"按修复后现状重写(硬件层模式中断断言恢复安全、NET 层/产品仍按"netTask 恒在轮询"设计);彻底方案(`.ioc` 移除)经评估未采用。同日构建验证发现两处**存量**测试固件缺陷(与 P1-6 无关,A/B 对照确证):① `net_test_task.c` `s_rx_buf` 使用先于声明 → NET 层配置自 2026-08(88160a5)起不可编译;② 128KB 静态 RAM 预算溢出(产品镜像 123.6KB,HW 全阶段固件超 8.7KB、NET 固件超 ~28KB)——已立 P1-9/P1-10 跟踪 | diff --git a/docs/测试说明/相量层单元测试说明.md b/docs/测试说明/相量层单元测试说明.md new file mode 100644 index 00000000..27b19f4c --- /dev/null +++ b/docs/测试说明/相量层单元测试说明.md @@ -0,0 +1,215 @@ +# 相量层单元测试说明(主机侧) + +> **模块**:Phasor Layer(L2 相量计算,`App/algo/phasor.c` + `App/algo/sample_ring.c`) +> **测试类型**:**主机侧纯逻辑单元测试**(L0.5 层)——在 PC 上离线验证全周傅氏相量计算与 +> 采样环缓冲,零硬件、零烧录、毫秒级、不占 MCU 的 RAM。 +> **状态**:✅ 已落地(57 用例,`/W4` 0 告警,已挂 pre-commit 钩子) +> **日期**:2026-10-10 +> **关联文档**: +> [`../../AGENTS.md`](../../AGENTS.md)(平台约束)、 +> [`测试开发标准流程.md`](测试开发标准流程.md)(测试分层与门禁总览)、 +> [`ADC输入层单元测试说明.md`](ADC输入层单元测试说明.md)(同批落地的 L1 姊妹篇)、 +> [`../算法/傅氏算法详解.md`](../算法/傅氏算法详解.md)(**系数约定权威口径** §2.3)、 +> [`../算法/继电保护功能复现规划.md`](../算法/继电保护功能复现规划.md)(§3.2 L2 设计点 / §10.2 术语表)、 +> [`../硬件平台/ADC通道映射说明.md`](../硬件平台/ADC通道映射说明.md)(L1 输入口径 / N-18 约束) + +--- + +## 1. 概述 + +### 1.1 为什么单独做主机侧单测 + +`App/algo/` 是本项目**首个纯算法层**(L1/L2 落点)——**无寄存器、无 RTOS、无文件 / 外设依赖**, +因此天然适合离线穷举。板端套件(`test/board/`)**无法验证本层的核心判据**: + +- 板端**无信号源**,无法注入已知**幅值 / 相位 / 频率**的标准波形,因而无法验证 + "读数 = 设定值"这一根本判据(同 L1-3 的替代口径,见 `docs/硬件平台/ADC通道映射说明.md` §8); +- 谐波抑制、频率敏感度、滑动窗恒定性、双区无撕裂等特性在板上**无稳定激励源**可造。 + +主机侧可**合成任意波形**(含直流、任意次谐波、频偏、任意初相),把上述特性一次性钉死, +且**不占 MCU 的 RAM**(本项目 RAM 预算见 `docs/内存管理/RAM使用分析与优化方案.md`)。 + +### 1.2 被测对象(**无需任何桩**) + +两个源文件都是纯算法,测试单元直接 `#include` 即可,**没有桩目录**: + +``` +test/host/phasor/phasor_test.c + ├── #include "sample_ring.c" ← 生产源文件(App/algo/),原样纳入(采样环,L2 输入) + └── #include "phasor.c" ← 生产源文件(App/algo/),原样纳入(相量计算,L2 核心) +``` + +`include_dirs` 只需 `App/algo` + 本目录。源文件被纳入同一编译单元,故**静态量亦可直接观测** +(白盒):`s_zone[2][N]`、`s_use_zone`、`s_ready`、`k_fft_sin` / `k_fft_cos`、`phasor_zero()`。 + +### 1.3 关键约定:系数表已预乘 √2/N("直接即有效值") + +权威口径 `docs/算法/傅氏算法详解.md` §2.3:系数为 +`sin(2πk/N)·(√2/N)`、`cos(2πk/N)·(√2/N)`(N = 20)。因此 `fRe = a₁/√2`、`fIm = b₁/√2`,于是 + +- **`fAmSQ = √(fRe² + fIm²)` 直接就是有效值(RMS)**,无需再除 √2; +- `fAm = √2 · fAmSQ` 是正弦峰值。 + +判据是归一化恒等式 **`Σsin² = Σcos² = 1/N`**(`TC-PH-001/002` 逐项校验,防转录错)。 +术语权威见 `docs/算法/继电保护功能复现规划.md` §10.2: +`{fRe, fIm, fAm, fAmSQ}` = 实部 / 虚部 / **幅值** / **有效值**。 + +> 采样参数:N = 20 点/周波 @ 50 Hz → 1000 Hz,保护节拍 1 ms(与 L1 采样节拍同源)。 +> 系数表为**离线生成的 float 常量**(运行期不调 `sinf` / `cosf`),故主机侧与 MCU 侧 +> 使用**逐位相同**的系数——这是本单测结论能外推到真机的前提。 + +### 1.4 测试环境 + +| 依赖 | 版本 / 说明 | +|---|---| +| 编译器 | 本机 MSVC(VS2019/2022/2026 任一),由 `run_unit_test.py` 自动探测 | +| Python | 仓库 `.venv` 或任意 python3(**纯标准库**) | +| 硬件 | 无(不需要开发板 / 串口 / ST-LINK / 信号源) | + +工具链探测 / `INCLUDE`-`LIB` 拼接 / 编译运行 / 退出码判定全部委托通用 harness +[`../../test/host/common/unit_test_harness.py`](../../test/host/common/unit_test_harness.py), +本模块 `run_unit_test.py` 只**声明** `TestBuild`(源文件 / 汇总行标记)。 + +### 1.5 运行方式 + +```powershell +# 仓库根目录(统一用 .venv) +& '.venv\Scripts\python.exe' test/host/phasor/run_unit_test.py +``` + +退出码:`0`=PASS / `1`=测试失败 / `2`=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +### 1.6 回归接入 + +| 项 | 说明 | +|---|---| +| **触发范围** | `App/algo/phasor.c` / `phasor.h`、`App/algo/sample_ring.c` / `sample_ring.h`,或 `test/host/phasor/**`、`test/host/common/unit_test_harness.py` 变动 | +| **门禁层级** | **L0.5 层**(介于 L0 静态门禁与 L1 板端套件之间):主机侧、秒级、纯逻辑 | +| **pre-commit 钩子** | `phasor-unit-test`(本地钩子,`.pre-commit-config.yaml` §7d) | +| **汇总行** | `=== Phasor Tests: 57/57 PASSED (failed=0) ===` | +| **与板端测试的关系** | 互补:本单测保证**算法正确性**;板端保证 **1 kHz 采样节拍与 TPAFE 快照链路**(L1-1 / L1-2)正常 | + +--- + +## 2. 测试用例 + +> 命名 `TC-PH-NNN`(PH = PHasor)。用例内的输出标签使用 `L2-x` 前缀,对应 +> `docs/算法/继电保护功能复现规划.md` §3.2 的设计点编号。部分用例直接观测静态量(白盒)。 + +### 2.1 L2-1 全周傅氏系数表 + +| 用例 | 断言 | 设计点 | +|---|---|---| +| **TC-PH-001** | 逐项:`sin[k] == sin(2πk/N)·(√2/N)`、`cos[k] == cos(2πk/N)·(√2/N)`(k = 0..19) | L2-1 防转录错 | +| **TC-PH-002** | 归一化 `Σsin² == Σcos² == 1/N`;精确零点 `sin@0,10`、`cos@5,15`;`sin[5] == √2/N` | L2-1 判据基础 | + +### 2.2 L2-3 / L2-4 基波幅值 / 有效值 / 相位 + +| 用例 | 断言 | 设计点 | +|---|---|---| +| **TC-PH-003** | 纯基波:`fAmSQ == A/√2`(**有效值**)、`fAm == √2·fAmSQ`(幅值),误差 ≤ 1% | L2-3 | +| **TC-PH-004** | `atan2(fIm, fRe)` == 注入相角(6 组幅值 / 相位) | L2-4 | + +### 2.3 直流与谐波抑制 / 任意次谐波提取 + +| 用例 | 断言 | 设计点 | +|---|---|---| +| **TC-PH-005** | 直流 + 2 / 3 / 5 次谐波被**完全滤除**(基波读数不变,误差 ≤ 1%,5 例) | L2-1 | +| **TC-PH-006** | 任意次谐波提取(**索引步进 = 谐波次数**):k = 1 / 2 / 5 从混合信号中提取;纯基波下 2 / 5 次分量 ≈ 0 | L2-1 / L1-9 入口 | + +### 2.4 频率敏感度 + +| 用例 | 断言 | 设计点 | +|---|---|---| +| **TC-PH-007** | 幅值误差:50 Hz ≤ 0.1%、±1 Hz ≤ 1.5%、±2 Hz ≤ 3%(**最坏相位**) | L2-3 / L1-7 依据 | + +### 2.5 L2 输入采样环(`sample_ring.c`) + +| 用例 | 断言 | 设计点 | +|---|---|---| +| **TC-PH-010** | `init` 清填充与帧计数;未攒满取窗 → -2;入环后取窗**按时序**(老 → 新);通道之间**互相独立**;填充量饱和于 `DEPTH` 但帧计数**不**饱和;回绕后取窗 == 最后 N 个值(有序);参数守卫:`n=0` / `n>DEPTH` / 越界通道 / `NULL out` / `NULL frame` / 错帧长 均拒绝 | L2 输入 | + +### 2.6 L2-2 实例层 + +| 用例 | 断言 | 设计点 | +|---|---|---| +| **TC-PH-020** | 实例数 == 5;实例表可取 `name`;越界实例 → `NULL`;未攒满一周波 `phasor_get()` → -2;越界 index / `NULL out` → -1;攒满 20 帧后 `get()` == 0 且实例 0 的 RMS == `A/√2`;谐波实例(`C04-IA-F2` / `C05-IA-F5`)在纯基波下 ≈ 0;**逐实例复核"实例窗 == 独立 `phasor_calc()` 结果"** | L2-2 | + +### 2.7 L2-6 滑动窗 + +| 用例 | 断言 | 设计点 | +|---|---|---| +| **TC-PH-021** | 连续滑动 20 拍,基波读数**恒定**(误差 ≤ 1%) | L2-6 | + +### 2.8 L2-7 双区指针原子交换 + +| 用例 | 断言 | 设计点 | +|---|---|---| +| **TC-PH-022** | 使用区逐次产出**交替** 0 → 1 → 0 → 1;**连测 60 拍**:每拍 `get()` == 当前环窗的独立重算(幅值每 20 拍在 100 / 10 之间切换 → 读到陈旧区会立刻不相等);扫描后结果仍可读 | L2-7 | + +### 2.9 `phasor_zero` / `phasor_init` 清零与幂等 + +| 用例 | 断言 | 设计点 | +|---|---|---| +| **TC-PH-030** | 直接白盒调 `phasor_zero()` → 四字段(含 `fAm` / `fAmSQ`)全清零 | 静态辅助 | +| **TC-PH-031** | 先把双区写脏 → `phasor_init()` 后**双区全零** | init 清零 | +| **TC-PH-032** | `phasor_init()` 后使用区复位为 0 | init 复位 | +| **TC-PH-033** | `phasor_init()` 后就绪标志清(`phasor_get()` → -2) | init 复位 | +| **TC-PH-034** | 重复 `phasor_init()` 幂等 | 可重复调用 | + +**当前结果**:57/57 PASSED(`failed=0`)。 + +--- + +## 3. 如何扩展用例 + +1. 加一个 `static void tc_xxx(void)`;用 `gen_sine()` 造正弦窗、`frame_at()` 造整帧(喂实例层); +2. 驱动实例层三步:`sample_ring_push_frame()` → `phasor_run()` → `phasor_get()`; +3. `CHECK(cond, "描述")` 断言;在 `main()` 里注册(并同步更新本文档 §2 与变更记录)。 + +> ⚠ 实例层是**全局态**(双区 + 采样环),跨用例必须先 `phasor_init()`——它会同时 +> `sample_ring_init()`,等价于"刚上电"。否则上一用例的环内容会串进来。 + +--- + +## 4. 已知边界与设计取舍 + +### 4.1 采样环用"暴力重算",不做迭代 + +每来一个新点,`phasor_run()` 对 20 点**整体重算**(滑动窗)。迭代法(加减首尾项)会累积 +浮点误差且难以保证与"整体重算"逐位一致;暴力法的代价是每拍 `N × 实例数` 次乘加 + 取模, +在 1 ms 节拍下仍有余量。本单测的 `TC-PH-022`(每拍 `get()` == 独立重算)正是这条取舍的 +**回归防线**——一旦有人改成迭代法,逐位一致性断言会立刻失败。 + +### 4.2 主机侧合成波形**不代表真实硬件** + +`frame_at()` 给 9 路语义通道灌的是**同值**波形,仅为驱动 L2 本身。V0.1 上各通道**不同刻** +(N-18:TPAFE 与 STM32 无共同采样触发),跨通道相量运算在本平台上不成立——本单测**不**做 +任何跨通道合成断言(L2-5 受限,见 `docs/算法/继电保护功能复现规划.md` §1.6)。 + +### 4.3 实例数 4 → 5:规划表已对齐(2026-10-10) + +**参考项目**(`APP/CLS100_Dsp/InstCalCell.c`)为 **4 个实例**——C01 基波 I/U、C02 零序基波、 +C03 二次谐波、C04 五次谐波;**本项目**按 V0.1 **有效通道**(`UA`/`UN`/`IA`,见规划 §1.6: +`UB/UC/IB/IC/IN` 无中点偏置 → 削顶不可用)展开为 **5 个实例** +(`C01-IA-F1` / `C02-UA-F1` / `C03-UN-F1` / `C04-IA-F2` / `C05-IA-F5`)。 + +`docs/算法/继电保护功能复现规划.md` §3.2 的 L2-2 行已于 2026-10-10 同步为 **5 个实例** +(并加注说明 4 → 5 的来历);本单测按**实现**断言(实例数 == 5),两者口径现已一致。 +`docs/算法/傅氏算法详解.md` §7 的"4 个实例"已标注为**参考项目**口径。 + +### 4.4 实例表用"数据驱动"而非硬编码 + +`k_inst[]` 每项为 `{name, sem, harm}`,`phasor_run()` 遍历该表取窗并计算。因此新增一个 +相量实例(如 L2-8 的遥测谐波轨)**只需在表里加一行**,本单测的"实例数 == 5"与 +"逐实例复核"两条断言会**自动扩展到新实例**——但需同步更新该常量断言。 + +--- + +## 5. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-10 | 创建;52 用例(TC-PH-001..022)全绿;覆盖系数表 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 实例层 / 滑动窗 / 双区交换 | +| 2026-10-10 | 补 TC-PH-030..034(`phasor_zero` 四字段清零 + `phasor_init` 双区清零 / 使用区与就绪标志复位 / 幂等)→ 57 用例;函数级覆盖审计 `App/algo` 三源共 20 函数、0 未覆盖;挂 pre-commit 钩子 `phasor-unit-test` | +| 2026-10-10 | §4.3 由"与规划表的一处待对齐"改为"实例数 4 → 5:规划表已对齐"——规划 §3.2 的 L2-2 行同步为 **5 个实例**(并加注 4 → 5 的来历),`傅氏算法详解.md` §7 的"4 个实例"标注为**参考项目**口径 | diff --git a/docs/测试说明/看门狗单元测试说明.md b/docs/测试说明/看门狗单元测试说明.md index 1111137e..897d41d6 100644 --- a/docs/测试说明/看门狗单元测试说明.md +++ b/docs/测试说明/看门狗单元测试说明.md @@ -93,7 +93,7 @@ test/host/watchdog/watchdog_test.c | **门禁层级** | **L0.5 层**(介于 L0 静态门禁与 L1 板端套件之间):主机侧、秒级、纯逻辑,比板端套件便宜、比静态检查更强 | | **pre-commit 钩子** | `watchdog-unit-test`(本地钩子,`.pre-commit-config.yaml` §7) | | **汇总行** | `=== Task Watchdog Tests: N/N PASSED (failed=0) ===` | -| **与板端测试的关系** | 互补:本单测保证**判定逻辑**正确;板端 `@test` 故障注入(V2/V4,见 `子系统设计/看门狗设计说明.md` §7 验证记录)保证**真机复位链路**正确 | +| **与板端测试的关系** | 互补:本单测保证**判定逻辑**正确;板端 `@test` 故障注入(V2/V4,见 `docs/子系统设计/看门狗设计说明.md` §7 验证记录)保证**真机复位链路**正确 | --- diff --git a/docs/测试说明/自动化测试流程.md b/docs/测试说明/自动化测试流程.md index 46510688..4e3d1e71 100644 --- a/docs/测试说明/自动化测试流程.md +++ b/docs/测试说明/自动化测试流程.md @@ -37,6 +37,7 @@ - `1` = FAIL:存在缺失标记(`[MISS]`)或违规模式(`[HIT]`) - `2` = 日志文件不可读 - **固件身份戳(2026-10-10 加)**:`App/app/app_main.c` 开机打印 `fw build: <__DATE__> <__TIME__>`(编译期展开成一段**连续 ASCII**,`.hex` 里可原样搜到)。`run_boot_test.py` 从**待烧的 hex** 里抠出该串并追加成 Required 标记 → 芯片里跑的必须就是这份构建。反例实测:把 hex 里的戳改成 `Oct 9 2026 01:00:00` 再跑,其余标记全中但 `[MISS] fw build: Oct 9 2026 01:00:00` → FAIL——这正是过去"反复重下载、芯片一直是旧镜像"在闭环里**完全隐形**的场景。`--no-fw-check` 关闭;hex 里没有该串(旧构建)只 WARN 跳过、不假判。 +- **戳覆盖构建配置(2026-10-11 加)**:`__DATE__`/`__TIME__` 只在**该 TU 被重编**时才刷新——增量构建下只切 `test/board/test_config.h` 的套件宏**不会**重编 `app_main.c`,于是产品镜像与测试镜像顶着同一个戳(实测:切宏后的测试构建与产品构建戳同为 `Oct 11 2026 14:44:11`,只有回读校验才发现镜像不同)。两条防线:① `app_main.c` `#include "test_config.h"` 建立头依赖 → 改宏必重编本 TU、戳必刷新;② 戳串追加构建变体 ` suite= dstr=`(`FW_BUILD_VARIANT`)→ 产品 = `suite=NONE dstr=off`、CH395F 测试 = `suite=CH395F dstr=on`,**即便戳时间相同也能一眼分辨**。实测:切宏前后 `Oct 11 2026 20:36:11 suite=NONE dstr=off` → `Oct 11 2026 20:36:40 suite=CH395F dstr=on`;`@test` 闭环对新格式匹配通过(Required 行就是完整戳串,脚本按"标记后连续可打印 ASCII"抠取,无需改脚本)。 - **烧录回读(2026-10-10 加)**:采集结束后自动跑 `stlink_flash.py cmp`(= `-CmpFile` 全片逐字节比对),判定按**行首** `No difference found`(进度条与帮助文本里也含该短语,故不能全文匹配);不一致 → `RUN_TEST_ERROR: flash readback mismatch` + 退出码 2(本次运行作废:日志是"某个未知镜像"跑出来的)。⚠ **顺序不能颠倒**——`-CmpFile` 连接时会复位目标(实测板子当场重打一遍开机日志)。`--no-verify-flash` 关闭。 - **启动锚点计数(2026-10-10 加)**:`reset cause:` 在一次采集里出现 **0 次**(采集没从复位开始,标记全中也可能只是上一轮残留)或 **>1 次**(板子中途自己复位:IWDG 咬狗 / Fault 后 `NVIC_SystemReset` / 掉电重启)→ FAIL;`--max-boot-count N` 放宽(跑故意复位的套件时用),`--expect-reset-cause 'software;power-on'` 对第一个锚点做原因白名单(SWD 软复位实测为 `software (NVIC_SystemReset)`,CSR=0x14000000)。 - **判定可读性 + 时序(2026-10-10 第三批)**: @@ -65,6 +66,7 @@ | `ADC init:` | `App/app/app_main.c` | | `SD2506 ID:` / `time:` / `temp:` / `battery:` | `App/app/app_main.c` | | `Network Init` / `net init: OK` / `MAC:` / `PHY:` | `App/task/net_task.c` | +| `phasor(first)` / `phasor(hb)` | `App/task/adc_task.c`(L2 相量层,2026-10-11 起常开;`hb` 每 10 s 一次,采集窗口需 ≥10 s——默认 `max_wait 30 s` 覆盖) | **默认 Forbidden 模式**:`FAIL;ERROR;HardFault;[ERR]` @@ -80,8 +82,8 @@ | daemon 无响应 / 自动拉起失败 | 查 `.dsh/skills/serial-assistant/scripts/state/daemon.log`;`serial_tool.py restart` 重建 daemon;检查 COM 是否被其他程序占用 | | 抓到日志缺 `Network Init` | `-SettleMs` 太小(boot 任务间有 ~3-4s 自然静默间隙),用默认 8000ms;2026-10-10 起首字节到达前不启动静默计时(`--first-wait-ms` 默认 12000),首行晚到不再被提前收尾 | | 烧录后无数据(`no_data`) | 2026-10-10 起 `no_data` = **本次等待期间零字节**(旧实现比 daemon 累计字节,常驻 daemon 下永不触发,只能靠"日志里没有 [RX] 行"兜底);开串口 + clear 必须在烧录**前**完成(`run_boot_test.py` 已保证);核对 COM 号、板子供电,看 `state/serial.log` 有无 SYS 异常行 | -| `[MISS] fw build: <戳>`(其余标记全中) | 芯片里跑的不是这份构建:烧录没落地 / 烧错文件 / hex 未重编译。处置:`stlink_flash.py flash ` 重烧并看 `READBACK: OK`;确认 `@build` 已重跑(戳取自编译时刻) | -| `READBACK: FAIL` / `flash readback mismatch` | 芯片内容与 hex 逐字节不一致(写入未落地、选项字节/RDP 异常、烧错目标)。处置:按 `docs/测试说明/ST-LINK_CLI_复位使用说明.md` §10 排查;`stlink_flash.py flash` 重烧后闭环会自动复核 | +| `[MISS] fw build: <戳>`(其余标记全中) | 芯片里跑的不是这份构建:烧录没落地 / 烧错文件 / hex 未重编译。处置:`stlink_flash.py flash ` 重烧并看 `READBACK: OK`;确认 `@build` 已重跑(戳取自编译时刻)。⚠ 若戳**命中**但里面的 `suite=` 与本次要跑的套件不符(想跑 CH395F 却显示 `suite=NONE dstr=off`)→ 烧的是产品镜像,`@build` 不会替你切宏(→ `board-test-suite` skill) | +| `READBACK: FAIL` / `flash readback mismatch` | 芯片内容与 hex 逐字节不一致(写入未落地、选项字节/RDP 异常、烧错目标)。处置:按 `docs/测试说明/ST-LINK_CLI_复位使用说明.md` §7 排查;`stlink_flash.py flash` 重烧后闭环会自动复核 | | `boot banner x2 > allowed 1` | 采集窗口内板子自己复位过(IWDG 咬狗 / Fault 后 `NVIC_SystemReset` / 掉电重启)。处置:看判定里逐条打印的 `reset cause:` 定位;跑**故意复位**的套件用 `--max-boot-count N` 放宽 | | `marker order violated` | 标记全中但**先后顺序颠倒** = 某步在另一步之后补做(就绪屏障失序 / 失败重试补跑)。处置:看判定打印的各标记首次行号定位;`--require-order` 只在需要强约束时启用 | | `boot time N ms > limit` | 能开机但慢(mount 重试 / PHY 重协商 / 复位重试)。实测基线:`net init: OK` 在 **+537ms**(2026-10-10 台架);默认只报告不判(`--max-boot-ms 0`),要设门禁再给预算 | @@ -122,3 +124,4 @@ | 2026-10-10 | 采集链可靠性修复(serial_tool 第一批):`wait-idle` 的 `no_data` 改按本次增量判(旧实现常驻 daemon 下永不触发,"烧录后无数据"这一失败模式实际测不出来)+ 首字节门 `--first-timeout-ms`(`run_boot_test.py` 新增 `--first-wait-ms`,默认 12000);`log --grep` 改先过滤后截尾(旧顺序导致 marker 必然查不到)并加 `-i`/`--regex`;`wait-idle` CLI 超时自动推导,不再需手工 `--timeout`;HTTP 口独占绑定修正(旧选项名/数值均无效,实际未生效) | | 2026-10-10 | **第二批:闭环补上"烧进去的是哪份镜像"与"跑的过程中有没有偷偷复位"**:① 固件身份戳——`App/app/app_main.c` 开机打印 `fw build: <__DATE__> <__TIME__>`,`run_boot_test.py` 从待烧 hex 里抠出该串追加成 Required 标记(旧镜像/错镜像此前所有固定标记照样全中);② 烧录回读校验进闭环——`reset.bat` 新增 `cmp` 子命令(`-CmpFile` 全片比对),采集后自动跑,不一致 exit 2;实测 `-CmpFile` 连接时会复位目标,故必须排在采集之后;③ 启动锚点计数——`verify_log.py` 新增 `--max-boot-count`/`--boot-marker`/`--expect-reset-cause`,`reset cause:` 出现 0 次或 >1 次判 FAIL;④ 修 `--flash-cmd` 带参数时预检必 missing、以及 `cmd /c` 对含空格路径(本仓库目录名 `DTU Code`)的引号解析把回读假判成"芯片不一致"(改为直接 CreateProcess 跑 .bat)。真机验证:完整烧录闭环 PASS(含戳匹配 + `No difference found` + 锚点 x1);伪造戳 → 其余标记全中仍 FAIL | | 2026-10-10 | **第三批:判定可读性 + 时序 + 采集卫生**:① Forbidden 命中逐行打印行号+内容(旧只报条数);② boot_log 每行加 `[+Nms]` daemon 毫秒时基(`--raw-log` 关闭),据此量开机耗时(实测 `net init: OK` = **+537ms**;`--max-boot-ms` 默认 0 只报告);③ `--require-order` 标记先后顺序校验(全中但失序此前不可见);④ `--json` 结构化判定(退出码不变);⑤ `--keep-out`(默认 20)清理 out 目录旧产物(本机曾堆到 31+7 份);⑥ daemon 侧:无换行残行 64KB 封顶 + open/close/clear 会话边界丢弃残行 + 截头按行边界对齐(旧实现截出来的首行是半行)+ `open` 不带 `--dtr/--rts` 时保持现状、显式给则现场应用;⑦ `ports` 回 `configured_present`/`hint`,`status` 回 `rx_age_ms`。selftest 23/23、verify_log 离线回归 17/17、真机闭环 PASS | +| 2026-10-11 | **身份戳覆盖测试宏**(N-31 复测时发现):切 `test/board/test_config.h` 的套件宏后 `app_main.c` 未被重编,测试镜像与产品镜像顶着同一个 `fw build:` 戳 → 戳校验对"两种构建互换"失明(当时只有 `-CmpFile` 回读发现镜像不同)。修复:`app_main.c` `#include "test_config.h"`(头依赖 → 改宏必重编、戳必刷新)+ 戳串追加 ` suite= dstr=`(`FW_BUILD_VARIANT`,戳内容本身区分构建)。实测:`20:36:11 suite=NONE dstr=off` ↔ `20:36:40 suite=CH395F dstr=on`;`@test` 闭环对新格式 PASS(脚本无需改动,戳按"标记后连续可打印 ASCII"抠取)。失败模式表补一行:戳命中但 `suite=` 与要跑的套件不符 = 烧了产品镜像 | diff --git a/docs/测试说明/链路延迟测试说明.md b/docs/测试说明/链路延迟测试说明.md index 0ffdedf5..98e295a7 100644 --- a/docs/测试说明/链路延迟测试说明.md +++ b/docs/测试说明/链路延迟测试说明.md @@ -26,7 +26,7 @@ MCU 侧真实处理只占 6.7ms,任务拾取/调度约 2.3ms(2026-09-22 重 非 Windows 用"入站轻推"兜底(**29.4ms**,2.3×);协议与固件都不需要改(§7.4)。 - **设备侧无可行解**(帧长 3209 为质数、TX FIFO = MSS、CH395F 无 Nagle 开关)→ 不必再投入。 - **缺陷**:D10(断线重监听 588ms 冻结 netTask)、E-02(一条空闲 FTP 控制连接冻结全网 17.7s)、 - D17(INIT 应答帧字节数字段恒 0)等已量化并**修复 + 台架复验**(§8.1); + D17(INIT 应答帧字节数字段恒 0)等已量化并**修复 + 台架复验**(§8); D1(重传周期 500→200ms)/ D6(per-recv 日志)2026-09-22 已实施(§10.1);D20 已定位未修(§8.2)。 --- @@ -444,7 +444,7 @@ med 65.0ms/max 133.5ms(133.5 = 压力启动瞬态单帧);**L3 pending 化 ¹ L5 两行为 2026-09-22 上午分片时代(M2 B2 档)数值:L3 对 DONTWAIT 快速路径**逐位不变**(dispatch → `net_poll` → `get_recv_len` 读,无 readable 门控),pending tick 无 pending 时为纯指针读零 SPI -(µs 级)——机制上不影响 L5;M2 重验未在本轮执行(低风险,留待 Phase 13 下次窗口,见 `docs/待处理内容.md` N-3)。 +(µs 级)——机制上不影响 L5;M2 重验未在本轮执行(低风险,留待 Phase 13 下次窗口,见 `docs/任务规划/README.md` N-3)。 数据来源:L1/L2 = §6.1;L3/L5/L6/L7 = M2 B2 档(200ms、per-recv 日志已删)串口 `[LAT]` 行 (`m2_data/m2_quiet_r200.json`);L8/L9/L11 = **L3 pending 构建 2026-09-22 16:42/16:43 重锁** @@ -642,7 +642,7 @@ sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复 |---|---|---| | 每帧都**稳定**慢 ≈150~300ms | 结构性基线(§10.1 的 D2/D5/D6/D8) | 跑 §5 的 Phase 13/14 对照基线 | | **间歇**卡,且 ≈200ms 的整数倍(旧固件 500ms) | **D1** 丢包重传 | Wireshark `tcp.analysis.retransmission`;查 PHY/网线/Wi-Fi | -| **间歇**整屏冻结数秒~数十秒 | **E-02** 头阻塞(已修)/ D10 重连(已修)/ 回归 | 查 FTP/对时会话是否挂着;核对 §8.1 修复在位 | +| **间歇**整屏冻结数秒~数十秒 | **E-02** 头阻塞(已修)/ D10 重连(已修)/ 回归 | 查 FTP/对时会话是否挂着;核对 §8 修复在位 | | 只在开 FTP / 对时后卡 | E-02 + 单连接 socket 争用 | 临时停 ftpTask/timeSyncTask 对照 | | 换网口/换线/换 PC 就好 | D7 物理链路 | §10.2-S3 拓扑纪律 | @@ -652,20 +652,20 @@ sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复 |---|---|---| | L1/L2 超 crit | SPI 走线/PCB/SPI2 分频或 DMA 配置 | 对照 `docs/问题记录/CH395F_Trap_Records.md`;逻辑分析仪抓 SCK/CS | | L3 spike ≥200ms(连续台阶;旧固件 500ms) | **D1 重传 / D7 链路** | Wireshark `tcp.analysis.retransmission`;查 PHY 协商/网线/Wi-Fi | -| L3 基线(非 spike)抬高 | 芯片发送路径 / 中断响应 | 对照 CH395F 手册 §5.17/5.18(注意:状态寄存器**不被 SET 类命令更新**,`GET_CMD_STATUS` 恒读到 RESET 的 0x10 陈旧残留,2026-09-22 台架实证——不可用作 SET 值验收) | +| L3 基线(非 spike)抬高 | 芯片发送路径 / 中断响应 | 对照 `docs/芯片手册/CH395F.md` §5.17/5.18(注意:状态寄存器**不被 SET 类命令更新**,`GET_CMD_STATUS` 恒读到 RESET 的 0x10 陈旧残留,2026-09-22 台架实证——不可用作 SET 值验收) | | L5 压力档爆表 | **E-02 头阻塞(已修)→ 回归** | 定位触发源(阻塞 recv 的 FTP/对时);核对 L3 pending 在位(`net_recv_locked` 阻塞分支 = 登记 pending 而非等待;`net_pending_tick` 在主循环) | | L5/L6 静默档抬高 | D5 调度(任务抢占/节拍) | 查任务优先级与 osDelay;`stack_report.py` 核对栈水位 | | L6/L7 整体抬高且 L1~L5 正常 | 线路+PC 段(L4) | Wireshark 段间间隙;`Δ` 对照定位 MCU 侧净贡献 | | **L7/L8 尾批固定迟到 ~50ms** | 已定性(§7.3):次 MSS 尾段被 Nagle 扣住 + Windows 延迟 ACK | 客户端侧解法已实测(§7.4):`--ack-probe` / `--ack-fast` → tail 52→3.9ms;生产客户端已实装;**设备侧无解**,不必再投入 | -| **L5 出现重复投递**(收到 > 发送) | **D11**:芯片 TCP RX 饱和重复递交 | 降低灌入速率/加抽干(§8.1 D11) | -| **会话无征兆失步**(结果错位/挂死) | **D12**:net 层任务通知值语义 | 核对 `net_wait_result` 清除掩码全 1 + 按返回值判定(§8.1 D12/D12-a) | -| **L7 一直超时 / peer 报 `l7_crc.fail`** | **D15**:帧格式不一致 / peer 超读 | 对照 `ui_net_sink.c` 的 `remo_send`/`remo_handle`(§8.1 D15) | -| **会话跑完一段后无限循环、peer 空等 300s** | **D16**:Phase 13 状态机迁移目标错 | 核对顺序 L5→L3→L10→L6→L7→DONE(§8.1 D16) | -| **`[LAT]` 段行缺失/截断、wait-idle 判不出空闲** | **D12-b**:热路径日志洪水 | 删除 net 热路径逐调用日志(§8.1 D12-b) | +| **L5 出现重复投递**(收到 > 发送) | **D11**:芯片 TCP RX 饱和重复递交 | 降低灌入速率/加抽干(§8 D11) | +| **会话无征兆失步**(结果错位/挂死) | **D12**:net 层任务通知值语义 | 核对 `net_wait_result` 清除掩码全 1 + 按返回值判定(§8 D12/D12-a) | +| **L7 一直超时 / peer 报 `l7_crc.fail`** | **D15**:帧格式不一致 / peer 超读 | 对照 `ui_net_sink.c` 的 `remo_send`/`remo_handle`(§8 D15) | +| **会话跑完一段后无限循环、peer 空等 300s** | **D16**:Phase 13 状态机迁移目标错 | 核对顺序 L5→L3→L10→L6→L7→DONE(§8 D16) | +| **`[LAT]` 段行缺失/截断、wait-idle 判不出空闲** | **D12-b**:热路径日志洪水 | 删除 net 热路径逐调用日志(§8 D12-b) | | **开机日志连续丢行**(含必检标记 `net init: OK` 等)但系统健康 | **日志并发竞态**:多任务并发 `printf` → 非重入 `fputc`/`HAL_UART_Transmit` 丢整行、UART 状态短暂卡住(2026-09-22 台架实证:boot 时 testTask 启动行与 netTask 日志并发 → 连续 4 行丢失、~2s 后自恢复) | 必现证据行采用"静默采集 + 延迟到安静窗口打印"模式(范例:P0-a 验收行曾按此模式实装,验收完成后探针已移除);根治 = S8 日志非阻塞/串行化(§10.2) | -| **`relisten` 段 FAIL、断线固定 ~588ms** | **D10** 回归(退回 `osDelay(500)`) | 核对 `net_relisten_try`/`net_relisten_retry_tick` 非阻塞重试在位(§8.1 D10) | +| **`relisten` 段 FAIL、断线固定 ~588ms** | **D10** 回归(退回 `osDelay(500)`) | 核对 `net_relisten_try`/`net_relisten_retry_tick` 非阻塞重试在位(§8 D10) | | **远端显示整帧卡住十几秒后自恢复** | **E-02** 回归 | 核对 L3 pending 在位:`net_recv_locked` 阻塞分支登记 pending(哨兵 `NET_PENDING_REGISTERED`)+ `net_pending_tick` 在主循环 + 调用方剩余预算循环(lftpd/timeSync)(§8.3 要点 4) | -| **INIT 应答帧字节数字段恒为 0** | **D17** 回归 | 核对 `remo_send_init` 发满 8B(§8.1 D17) | +| **INIT 应答帧字节数字段恒为 0** | **D17** 回归 | 核对 `remo_send_init` 发满 8B(§8 D17) | | **开机首个 FTP 会话第 2 个数据连接 425** | **D20**(已定位未修,§8.2) | 复现:`test/host/ftp_regression.py`(复位开边界);现场规避:客户端重试 425(瞬态 4xx);根治方向:数据 Socket 轮换 | | **`[LAT]` 多段数值雷同 / 全 0 却 verdict=OK** | **D18/D19**(测试工装) | 核对 `lat_probe_add_seg` 段名过滤 + `lat_time_init` 调用 + `med==0` 判无效 | | **连接建立后 15~30s 无流量即被 RST** | **D13**:芯片空闲超时(芯片行为) | 客户端须容忍并重连(peer `--wait` 重试是正常路径) | @@ -677,13 +677,13 @@ sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复 ## 10. 结构性成本项与待办方向 -### 10.1 静态分析十项结构原因(D1~D10,独立于 §8.1 的台架缺陷编号)与当前状态 +### 10.1 静态分析十项结构原因(D1~D10,独立于 §8 的台架缺陷编号)与当前状态 | # | 原因 | 实测验证(2026-09-22 台架) | 当前状态 | |---|---|---|---| | D1 | CH395F TCP 重传固定周期(非 RTT 自适应) | 台架链路(有线 100M 全双工)多轮零重传尖峰 → 台架无丢包;**弱网(Wi-Fi/双工不匹配)仍是秒级卡顿首要嫌疑**,200ms 粒度台阶 = 重传指纹(旧固件 500ms) | **已修(P0-a,2026-09-22)**:`NET_RETRANS_PERIOD_MS` 500→**200**(窗口 8×200=1.6s,原 4s)。台架验收说明:`GET_CMD_STATUS` 对 SET 类命令无判别力(500/2000ms 超限对照均读 0x10 陈旧残留,§9.2)→ 接受性以手册范围(≤1000ms)+ 全套件零回归(§6.2/§6.3)+ 与 500ms 行为同构为准。验收探针(3×20ms 状态轮询 + 证据行)验收完成后已从代码移除,结论保留于本文档 | | D2 | 3209B × 1KB 发送 FIFO 停止等待(4 次串行分块握手,期间 netTask 全程占用) | `chunk_wait` 219µs/块 ≈ SPI 搬运成本的 1/7 → 握手本身**不是瓶颈**;每帧固定成本小(MCU 净 7.1ms) | 基线成本,可接受;根治 = 差量/RLE + 帧版本号(待办 S2) | -| D3 | netMsgQueue 头阻塞(= §8.1 E-02) | 已量化:一条空闲 FTP 控制连接冻结全网 17.7s | **两阶段闭环**:① 5ms 分片有界读 + 让出(f8d6e90/4a1605b,缓解)② **L3 pending 化**(`a45c0cb`,结构性——执行器零占用,缺陷类消灭,§8.3 要点 4) | +| D3 | netMsgQueue 头阻塞(= §8 E-02) | 已量化:一条空闲 FTP 控制连接冻结全网 17.7s | **两阶段闭环**:① 5ms 分片有界读 + 让出(f8d6e90/4a1605b,缓解)② **L3 pending 化**(`a45c0cb`,结构性——执行器零占用,缺陷类消灭,§8.3 要点 4) | | D4 | PC 端延迟 ACK 与"每段等 ACK"停止等待交互 | `--ack-fast` 带内实验证实:关掉延迟 ACK 尾批自行消失(52→3.9ms) | **已客户端侧解决**(生产客户端关延迟 ACK/轻推兜底,§7.4) | | D5 | uiTask 节拍:10ms 循环 + 每拍一次 64B recv + step 在 poll 前 | `ui_loop` 10986µs ≈ 设计值(渲染 ~1ms);拾取调度 ~2.3ms(P0-b 删 per-recv 日志后,原 ~6.8ms) | 基线行为;可选优化 S7(poll 前置 + while 排空),低优先 | | D6 | 阻塞式日志在关键路径(fputc 逐字节 @115200) | `logtax_line` 4402µs/行;per-recv 日志已删(2026-09-22):`queue_rt_recv` 5997→1223µs、帧周期 66.0→61.6ms、首字节 13.9→9.6ms | **部分已修(P0-b,2026-09-22)**;S8 非阻塞日志仍待办(同时是开机日志并发丢行 §9.2 的根治项) | diff --git a/docs/环境与部署/CI配置指南.md b/docs/环境与部署/CI配置指南.md index 0ef41c37..bde1f0ea 100644 --- a/docs/环境与部署/CI配置指南.md +++ b/docs/环境与部署/CI配置指南.md @@ -341,7 +341,7 @@ github.com 会在 checkout 步骤卡住/失败。三种处置: | `l0-gates` 报 `::error::runner 缺少:Python, cppcheck` | runner 没装或不在 PATH | 装完后**重启 runner 服务**(新进程才吃到新 PATH) | | `checkout` 步骤卡住/超时 | 拉不到 `actions/checkout@v4`(内网) | 见 §4.3 | | `l05` 日志出现 `::warning::runner 未安装 MSVC` | VS 装在非默认路径或未装 | 见 §3.2(默认路径 / 同步改两处根目录) | -| `mem-numbers` 显示 `Skipped` | 正常(CI 无 Keil,无 `.map`) | 无需处理;但注意 **CI 未守体积门禁**(见 §0.0),改代码后本地 `@build` + `check_mem_numbers.py --scan` 兜底 | +| `mem-numbers` 显示 `Skipped` | 正常(CI 无 Keil,无 `.map`) | 无需处理;但注意 **CI 未守体积门禁**(见 §0),改代码后本地 `@build` + `check_mem_numbers.py --scan` 兜底 | | L0.5 显示 `2 runner(s), X/Y ...` 但 X.log` | | L0.5 报 `produced no '=== ... N/M PASSED ===' summary line` | 该模块运行器未输出汇总行(改了 harness 或运行器崩溃) | 核对 `test/host/common/unit_test_harness.py` 的 `summary_marker` 与该模块 `TestBuild` 是否一致 | | `Tee-Object` 步骤报 `TerminatorExpectedAtEndOfString` / 字符串缺少终止符 | `run:` 块混入了非 ASCII(GBK 解码错位) | 见文件头 🔴🔴;`run:` 内必须纯 ASCII | @@ -358,7 +358,7 @@ github.com 会在 checkout 步骤卡住/失败。三种处置: - **钩子集变化**(`.pre-commit-config.yaml` 增删钩子):CI 自动跟随,无需改 workflow; - **`@build` / L1 / L2**:永远留在本地,不要往 CI 加(需 Keil 授权与真机); - **runner 长时间不可用**时:CI 只是"多一道网",**提交前的本地 `ci_local.py` 仍是 L0 的唯一保障**, - 不能因"已经配了 CI"放宽(见 `docs/待处理内容.md` 的部署前置警示块)。 + 不能因"已经配了 CI"放宽(见 `docs/任务规划/README.md` 的部署前置警示块)。 --- diff --git a/docs/环境与部署/并行任务工作树方案.md b/docs/环境与部署/并行任务工作树方案.md new file mode 100644 index 00000000..224ef741 --- /dev/null +++ b/docs/环境与部署/并行任务工作树方案.md @@ -0,0 +1,397 @@ +# 并行任务工作树工作流(worktree + `task-worktree` skill) + +> **这个文件是什么**:多任务并行开发的**机制规格**——如何为每个任务开一个互不干扰的工作空间、 +> 干完如何合并回主干、哪些资源必须串行、冲突怎么治理。 +> +> **状态**:**已落地并端到端实测**(2026-10-12)——skill 在 `.dsh/skills/task-worktree/`, +> 实测记录与暴露并修复的缺陷见 §11。 +> +> **决策基线**(2026-10-12 用户确认):工作树放**仓库同级 `_wt/`**;集成分支**沿用 `v2-on-ui`**; +> `done` 回收**不自动 push**(沿用「push 需用户明确许可」)。 + +--- + +## 1. 目的与范围 + +**要解决的问题**:多个任务同时推进时,若共用同一个工作目录,会互相踩——构建产物、日志目录、 +Python 环境、以及 `docs/任务规划/` 这类**全局唯一登记簿**都会被并发写坏。 + +**做法**:每个任务一棵 `git worktree` + 一条任务分支,任务之间物理隔离;干完由固定命令合回主干。 + +**范围**:只解决「开发期隔离 + 回收期合并」的机械流程。**不改变**既有的分支纪律 +(push 需许可)、门禁口径(pre-commit 30 钩子 + `@build` + `@test`)与文档约定。 + +--- + +## 2. 方案选型 + +| 方案 | 为什么不用 | +| ---------------------- | ----------------------------------------------------------------------------------- | +| 同一目录切分支 | 构建产物 / `.venv` / 日志目录 / `.workbuddy-ai` 全共享,两任务互相踩;且本仓库明令禁止用 `git checkout` 丢弃未提交工作 | +| 每任务 `git clone` | 对象库 234 MB × N 份、Git LFS 全量重复;克隆之间无法安全共享分支,推送关系也更绕 | +| `git stash` / 打补丁倒换 | 不具备并发安全性,冲突只能靠人工记忆 | +| **`git worktree`(采用)** | 唯一同时满足「共享对象库 + 独立工作目录 + 独立 HEAD/index」的机制;git 原生,本仓库已有先例 | + +**命名惯例来源**:`D:/Code/DTU Code/_v1pin-build`、`_v2oui-build` 曾是遗留 worktree(detached HEAD, +2026-09-19 建、已过期),本方案沿用其「仓库同级 + `_` 前缀」命名,但把散落的目录收敛到统一的 +`_wt/` 容器下。**两者已于 2026-10-12 清理**:`_v1pin-build` 干净直接移除;`_v2oui-build` 有 2 个 +未提交改动(uvprojx / freertos.c),已按字节留档于 `_wt/.archive/2026-10-12-legacy-worktrees/` +后才移除(两提交由 `wip/trap22-fix` / `wip/v2-pending-recv` 持有,无提交丢失)。 + +--- + +## 3. 目录与资源分配 + +### 3.1 目录布局 + +```text +D:/Code/DTU Code/ +├── STM32F4-Base/ # 主工作树 = 集成分支 v2-on-ui(唯一检出主干的地方) +│ ├── .git/ # 共享对象库 + hooks(worktree 自动共享,无需任何操作) +│ ├── .venv/ # 唯一实体(Python 环境) +│ ├── .workbuddy-ai/ # 唯一实体(记忆;**不**共享给任务树,见 3.2) +│ └── simulator/SDL2/ # 唯一实体(模拟器 SDK) +└── _wt/ # ★ 并行任务容器(不在任何 git 仓库内) + ├── .state/.json # 任务台账:分支 / 联接 / 门禁与台架戳 + ├── .state/locks/ # 独占锁:build.lock(UV4)+ bench.lock(COM6/ST-LINK/设备 IP) + └── -/ # ← 一棵工作树(分支 task/-) + ├── .git # 文件(指向共享对象库,git 自动创建) + ├── .venv # → junction 到主工作树 + ├── simulator/SDL2 # → junction 到主工作树 + ├── .workbuddy-ai # 本任务自己的(清场前归并回主工作树) + └── MDK-ARM/… # 本任务独占的构建产物 +``` + +**工作树绝不能建在仓库内部**(`STM32F4-Base/` 里):那会变成未跟踪目录,污染 `git status`、 +`pre-commit --all-files` 与全库搜索。故统一放仓库同级的 `_wt/`。 + +### 3.2 共享 vs 独占 + +| 资源 | 归属 | 原因 | +| ------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------- | +| `.git/` 对象库 + hooks | **共享**(git 机制自动) | 工作树共享 common dir,提交门禁无需在新空间重装 | +| `.venv/` | **共享**(junction) | 体积大、内容与任务无关;每任务重建一份纯属浪费 | +| `simulator/SDL2/` | **共享**(junction) | 下载型 SDK,与任务无关 | +| `.workbuddy-ai/` | **独占**(刻意不共享) | ① 避免并发写同一份日志丢更新;② 不给联接递归删除的机会——它是本项目最不可再生的目录。任务侧记忆由清场前归并回主工作树 | +| `MDK-ARM/` 构建产物、`simulator/build/` | **独占** | 正是需要隔离的部分:两个任务可各自构建互不干扰 | +| `.serialtest/config.json`、`.dsh/skills/`、`.pre-commit-config.yaml` | **独占但内容一致** | 受版本控制,随检出白拿,无需 bootstrap | +| `.mnemon/`、`ci-artifacts/` | **独占** | 本地运行态,gitignored | + +### 3.3 junction 规则(**不能用 symlink**) + +本仓库 `core.symlinks=false`:git 里的符号链接会被检出成「内容是目标路径的普通文本文件」, +不是链接。因此**共享 `.venv` 只能靠 Windows 目录联接(junction)**,且 junction 必须创建在 +**版本控制之外**(`_wt/-/` 里的那几个路径都不在 git 索引内,安全)。 + +创建方式用 `mklink /J`(**不需要管理员权限**,也不同于符号链接)。实测(2026-10-12): +用 ctypes 直调 `DeviceIoControl(FSCTL_SET_REPARSE_POINT)` 会被系统拒绝(Win32 4392), +缓冲区经 hexdump 逐字节核对无误仍失败,而 `mklink /J` 正常 → 采用后者;参数以**列表** +交给 subprocess,含空格的路径(本项目 `DTU Code`)由 subprocess 自动加引号。 + +**摘除联接必须用 `os.rmdir`**(= `RemoveDirectoryW`,只摘链不递归)。实测 `.venv` 条目数 +摘除前后一致(2809 → 2809)。**清场顺序铁律:先摘联接,再 `git worktree remove`**—— +否则 git 可能递归进联接,把主工作树的 `.venv` / SDL2 一起删掉。 + +> ⚠ 不要在仓库里提交任何指向 `../` 的 symlink 来实现共享——在本机就是坏文件。 +> ⚠ MSYS 的 `rmdir` 拒绝摘除联接(把它当符号链接),手工清场要用 Python 的 `os.rmdir` +> 或直接走本 skill 的 `abort` / `done`。 + +--- + +## 4. 分支模型 + +| 项 | 取值 | +| -------- | ---------------------------------------------------------------------------------------------------- | +| 集成分支(主干) | `v2-on-ui` | +| 任务分支 | `task/-`,从主干当前 HEAD 切出 | +| `` | **沿用 `docs/任务规划/README.md` 的编号空间**(`P0-x`/`N-x`/`S-x`/`T-x`/`B-x`/`C-x`/`D-x`);无对应条目的临时任务用 `X-<日期>-` | +| 合并方式 | `git merge --no-ff` 合入主干 | +| 并发上限 | **同时最多 3 棵工作树** | + +**`` 复用登记簿编号**是为了可追溯:任务分支名直接对应待办条目,回收时销项有据可查。 + +**并发上限 3** 的依据:`.git` 已 234 MB,每棵工作树要检出约 2400 个受控文件(含 Git LFS 图片), +并发太多磁盘与 I/O 都不划算,人工也管不过来。 + +--- + +## 5. 任务生命周期 + +| 阶段 | 动作 | 并行性 | +| ------ | -------------------------------------------- | ----------- | +| `new` | 建分支 + 工作树 + junction + 台账 | 可并行 | +| 编码 | 在新空间里改代码 | 可并行 | +| 门禁 | `scripts/ci_local.py`(L0 + L0.5,纯软件) | **可并行** | +| 构建 | `@build`(UV4 增量,需持 `build` 锁) | **必须串行** | +| 台架 | `@test` / 板级套件(需先持台架锁,见 §7) | **必须串行** | +| 提交 | 本地 `git commit`(多行正文用 `-F `) | 可并行 | +| `sync` | `rebase` 追平主干,把冲突控制在任务分支上 | 可并行 | +| `done` | 预演 → 合入主干 → 残留审计 → 重生成产物 → 门禁 → 清理工作树 | **必须串行** | +| `push` | 推送主干/任务分支 | **需用户明确许可** | + +**为什么 `done` 必须串行**:合并动作会移动主干引用,两处同时合入会互相覆盖;且主干只在主工作树检出。 + +--- + +## 6. 冲突治理 + +### 6.1 单写者文档 + +并行失败通常不是代码冲突,而是**多个任务同时改同一个「登记簿」**。以下文件在任务期间 +**禁止在任务工作树里修改**,一律由 `done` 回收阶段在主工作树统一处理: + +| 文件 | 冲突原因 | 回收时的处理 | +| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| `docs/任务规划/`(整个文件夹) | 全项目唯一登记簿,每个任务都要销项 | 由主工作树统一改,任务分支不碰 | +| `AGENTS.md`、`docs/README.md`(索引/登记类) | 每个任务都要在「按主题查哪份文档」表、`docs/README.md` §9.2 环境表、变更记录里**各加一行** → 两任务必冲突(2026-10-12 实测:主工作树里本任务的行与他任务的行已并排混在**同一 hunk**) | 由主工作树统一追加;提交时**只 `git add` 本任务的行**(`git apply --cached` 选择性入库),勿整文件 `git add` | +| `AGENTS.md` 的 `TASK_TABLE` 块 | 生成物,两个任务都抬栈就冲突 | 合并后重跑 `scripts/gen_task_table.py` **重新生成**,不手工并 | +| `MDK-ARM/STM32F407-Demo.uvprojx` | 新源文件注册 + UV4 自动改写 | 手工并 `` 条目 → 重跑 `registration-check` → `@build` | +| `.pre-commit-config.yaml` | 新增主机单测要加钩子 | 并集处理 | +| `docs/内存管理/RAM使用分析与优化方案.md`(及其 `mem-numbers` 快照) | `mem-numbers` 快照 | **绝不手工合数字**,合并后 `scripts/check_mem_numbers.py --write` 重生成 | + +### 6.2 合并残留审计(**必做**) + +双方都改过的文件(尤其 `.gitignore`)会被 git 自动并成**并集**,可能留下: + +- **死规则**:指向已删目录的忽略项 +- **重复条目**:同一规则出现两次 + +判据 = 拿 base / 两个 tip **三方对比**(`git show :` 逐项 `grep -F`), +**不要用 `tail -N` 目测**(会漏看被合并在中间的行),也**不要把合并前就存在的历史重复算进来**。 + +### 6.3 冲突时的解决位置 + +`done` 先做**非破坏性预演**:`git merge-tree --write-tree v2-on-ui task/-`。 + +- `rc=0`(无冲突)→ 继续合并 +- `rc≠0` → **在任务分支上** `git merge v2-on-ui` 解决冲突并提交,再重跑 `done` + +在一次性分支上解冲突,**永不污染主干**。 + +--- + +## 7. 资源串行化 + +### 7.1 台架锁 + +以下物理资源**各只有一份**,故 `@test` / 板级套件 / 烧录必须串行: + +- 串口 `COM6` @115200 +- ST-LINK SN `0B250D12A218303030303032` +- 设备 IP `192.168.1.100` + +机制:`_wt/.state/locks/{build,bench}.lock` 记录持有者(任务 ID + pid + 时间戳 + 备注)。 + +**锁不绑定进程存活**:`lock acquire` 本身是短命进程,而真正用资源的烧录/构建在另一个进程里跑。 +若按 pid 判活,acquire 一退出锁就被判为陈旧 → 锁形同虚设(实测踩到)。 +故判「陈旧」只看**时效**(`LOCK_TTL_HOURS` = 6 小时)加显式 `--force`;pid 仅作排障记录。 +同任务重复 acquire 幂等;他人持锁时 acquire 拒绝(`--wait` 可轮询等待)。 +`done` 前置检查会在任一锁被占用时拒绝回收。 + +> ⚠ 台架相关的既有陷阱(`--require-selftest-pass` 的陈旧台账、回读校验会复位目标) +> 不因引入工作树而改变,仍按 `docs/测试说明/自动化测试流程.md` 执行。 + +### 7.2 构建 + +`@build`(UV4 增量)在新空间里是**独占目录**,但 **UV4 不能两个实例同时 `-b`**(2026-10-12 用户确认, +无需实测)——因此构建**同样纳入串行锁**(`build`),与台架锁并列。 + +> 结论:任务期真正可并行的只有**编码与静态门禁**(`ci_local.py` 的 L0 / L0.5,纯软件、零硬件); +> `@build` 与烧录/台架都必须排队。 + +### 7.3 并行道与写范围规则(规划层 ↔ 执行层映射) + +规划层(登记簿 `docs/任务规划/W1-采集保护收尾.md`,任务卡五字段 = **DoD / 硬依赖 / 资源 / 写范围 / 状态**) +按**资源属性**把任务切进三条道,本方案(工作树 + 锁)是它的执行层: + +| 道 | 资源 | 并行性 | 锁 / 机制 | +|---|---|---|---| +| **台架道** | COM6 / ST-LINK / 设备(§7.1) | **串行**——约束资源,**永不空转**(约束优先,TOC):代码道在跑时必须为下一个台架任务备料 | `bench.lock` | +| **代码道** | CPU / 源码 | **并行**——worktree 独立空间;前提 = 写范围互不相交 + 强门禁(31 钩子 `ci_local`,同 Linux 内核子系统树并行而能合回的逻辑,见 `docs.kernel.org/process/2.Process.html` 的 subsystem tree 模型) | 无锁;分支 ≤2 天、绿即合(trunk-based 短分支) | +| **外部窗口** | 标准源 / 第三方主站等外部设备 | **Fixed Date 类**(Kanban 服务分类):窗口一到全道让路;窗口前 2 日特性冻结(只修稳定,Zephyr RC 语义 → `docs.zephyrproject.org/latest/project/release_process.html`) | `bench.lock` + 外部预约 | + +**四条规则**: + +1. **WIP ≤ 2**:每道最多 1 个进行中(典型 = 台架道 1 + 代码道 1),先完再开——登记簿需可见 + `⬜ 未开始 / 🟦 进行中` 两态,销项仍删行。 +2. **写范围相交 = 声明合并次序**:任务卡必须写写范围(目录级);相交不排队等待,而是**声明谁先合** + (后合者在主工作树 rebase 后继续),claim 时定。 +3. **分支存活 ≤2 天、绿即合**:合并走 `done` 预演(§6.3);单写者文档(§6.1)仍由主工作树统一处理。 +4. **里程碑不拖延、功能顺延下一轮**(时间驱动):里程碑的退出判据写在登记簿,判据不满足则该**功能** + 移入下一波次,里程碑本身按判据改期而非无限等待。 + +> 登记簿是规划层唯一权威(§2 铁律不变);本节只定义执行层映射,**不在本文排任务顺序**。 + +--- + +## 8. SKILL 设计 + +**名称**:`task-worktree` **短命令**:`@task`(沿用本仓库 `@build` / `@test` 的 `@` 惯例) + +**执行体**:`.dsh/skills/task-worktree/scripts/task_worktree.py`(纯标准库, +照 `keil_build.py` 的写法,整目录可拷到别的项目复用)。**已落地并端到端实测**(见 §11)。 + +| 子命令 | 作用 | +| ----------------------------------------------------- | ----------------------------------------------------------- | +| `new [--base B] [--lean] [--needs-bench]` | 建分支 `task/-` + `_wt/-/` 工作树 + 共享联接 + 台账 | +| `list` | 所有任务一览:分支 / 落后领先 / 脏文件数 / 台架状态 + 两个锁的归属 | +| `status ` | 单任务详情:未提交清单、相对基线漂移、台架记录、已建联接 | +| `sync ` | `rebase` 追平主干,把冲突控制在任务分支上 | +| `lock acquire\|release\|status ` | 独占资源锁(`--task` / `--wait` / `--force` / `--bench-pass`) | +| `done [--dry-run] [--skip-build] [--skip-gates]` | 预演 → 合入主干 → 残留审计 → 重生成产物 → 门禁 → 清场 | +| `abort [--force]` | 放弃任务(有未提交/未合并内容时不加 `--force` 会拒绝) | + +**环境变量**:`DTU_WT_ROOT`(任务容器位置)· `DTU_TASK_BASE`(默认基线)· +`DTU_INTEGRATION_WORKTREE`(集成点,默认主工作树;用于隔离演练/测试)。 + +**关键约束**: + +1. **`done` 只能在主工作树执行**——主干 `v2-on-ui` 检出在主工作树,git 不允许同一分支在两处检出。 +2. **`done` 前置检查主工作树干净**,脏则中止并提示(避免把未提交工作卷进合并)。 +3. **`done` 不含 `push`**:结束于本地合入 + 门禁全绿,打印提示等用户点头。 +4. `--needs-bench` 任务若没有台架 PASS 戳,`done` 拒绝合并。 +5. 门禁未全绿时 **`done` 保留工作树与分支**(只留下合并提交),便于修正或回退——不静默清场。 +6. 清场顺序:**先摘联接,再 `git worktree remove`**(见 3.3)。 + +--- + +## 9. 门禁与验收 + +| 层级 | 内容 | 时机 | +| ------- | ------------------------------------ | ------------------------------ | +| 任务期 | `scripts/ci_local.py`(L0 + L0.5,可并行) | 每个任务各自跑 | +| 任务期 | `@build`(0 错 0 警,**持 `build` 锁串行**) | 每个任务各自跑 | +| 任务期(可选) | `@test` / 板级套件(持 `bench` 锁) | 需真机验证的任务 | +| 回收期 | 重生成产物 → `ci_local.py` → `@build` | `done` 内强制(`--skip-gates` 可跳过) | +| 人工 | push 许可 | 回收后 | + +**`done` 的完成判据**:主干上 `git merge --no-ff` 成功 + `ci_local.py` 全绿 + `@build` 0 错 0 警 + +- 残留审计无死规则/重复条目 + 工作树已移除。 + +--- + +## 10. 已知风险与待验证项 + +| 项 | 状态 | 处置 | +| -------------------------------------------- | --------------------- | ------------------------------------------- | +| UV4 能否并发构建 | **已确认不可**(2026-10-12) | `build` 与 `bench` 一并纳入串行锁 | +| 跳过 Git LFS smudge 是否影响门禁 | **已实测**(2026-10-12) | 见下表:只省磁盘、不省时间,两种方式工作区都干净 → 默认全量,`--lean` 备选 | +| 主工作树当前不干净 | 已知 | `done` 的前置检查会拦下 | +| 遗留 worktree(`_v1pin-build` / `_v2oui-build`) | 已知 | 落地时 `git worktree remove` 清理,避免新旧两套并存 | +| **「主工作树绿、干净工作树红」的隐藏依赖** | **已实证**(2026-10-12) | 见 10.2。属**原任务**漏提交,非本 skill 缺陷 | +| `git worktree remove` 中途被中断 → 残留「裸目录」 | **已实证**(2026-10-12) | 见下方 10.3 | + +### 10.1 LFS 实测数据(2026-10-12,2396 个受控文件 / 707 个 LFS `*.jpg`) + +| 方式 | 建树耗时 | 检出体积 | `git status` | 参考图 | +| --------------------------------- | ------ | ----- | ------------ | ---------- | +| 全量 smudge(默认) | 7.25 s | 83 MB | 干净 | 真实 JPEG,可看 | +| `--lean`(`GIT_LFS_SKIP_SMUDGE=1`) | 7.05 s | 45 MB | 干净 | 指针文本 | + +**结论**:LFS 对象已在本地,smudge 几乎不耗时,跳过**只省约 38 MB 磁盘**。 +故默认走全量(保留参考图可读性),磁盘紧张时才用 `--lean`。 +另外两种方式在全新工作树里跑 `pre-commit`(含 `doc-links`)都**正常通过**, +证明 hooks 经 common dir 共享、门禁不因工作树而失效。 + +### 10.2 工作树是「提交物自洽性」的照妖镜(2026-10-12 实证) + +演练中 `done` 的门禁在干净工作树里报出 2 处失效引用,而**主工作树里同一门禁是绿的**: + +```text +docs\内存管理\RAM使用分析与优化方案.md:277 [路径引用] scripts/ram_baseline.tsv +docs\内存管理\RAM使用分析与优化方案.md:585 [路径引用] docs/内存管理/RAM预算台账.md +``` + +根因:该文档**已提交且无改动**,但它引用的这两个文件**从未 `git add`**(属另一在途任务), +只在作者的主工作树磁盘上存在 → `doc-links` 查的是**文件系统**,于是在主工作树里通过; +一旦换到干净工作树(本演练的 `_probe-base2`),文件不存在 → 必然 FAIL。 + +**判据与处置**:这不是本 skill 的缺陷,而是被工作树**逼出来的真问题**—— +「一个提交是否自洽」不能靠「在我的工作目录里能过」来证明。修复归属**引用方任务** +(补 `git add`,或给引用加 `[计划]` 前缀 / 写明「已删除」状态词,见 `docs/文档规范.md`)。 + +**副作用提醒**:这同时说明**共享主工作树里的绿门禁不可尽信**——只要有未提交文件躺在磁盘上, +门禁就可能对着「未提交的现状」放行。多任务并行期更该用工作树 + `done` 复核。 + +### 10.3 `git worktree remove` 中断会留下「裸目录」(2026-10-12 实证) + +清理遗留 worktree 时踩到:`git worktree remove --force` 被中断后,`_v2oui-build` 的 `.git` **已被删除** +(于是 `git worktree list` 显示 `prunable`),但目录里**还躺着 2774 个条目**没删完。 + +**判据**:看到 `prunable` + 目录仍存在 = git 已删掉 gitfile、删除未完成。此时那棵树**已不再是 worktree** +(git 不认它了),`git worktree remove` 无法再用,只能按普通目录删除。 + + +**处置**:`git worktree prune` 清台账 → 手工删目录。删之前必须**重新扫一遍 reparse**—— +理由它有 `.git` 时没有联接,不等于现在没有(尤其别让他人趁乱建了联接)。 +清除手段用 Python 自底向上删并顺带 `chmod +w`(MDK 产物有 2 个只读文件), +**切忌 `rm -rf`**(MSYS 会递归进联接,实测见 3.3)。 + +**已知不做**:不引入第二套并行机制(如 git submodule / 多克隆), +保持「同一能力只有一个实现」的收敛原则。 + +--- + +## 11. 落地清单与实测记录 + +| 动作 | 目标 | 状态 | +|---|---|---| +| 新建 | `.dsh/skills/task-worktree/SKILL.md`(含 `name` / `description` / `whenToUse` frontmatter) | ✅ | +| 新建 | `.dsh/skills/task-worktree/scripts/task_worktree.py`(纯标准库) | ✅ | +| 新建 | `.dsh/skills/task-worktree/.gitignore` | ✅ | +| 修改 | `AGENTS.md`「按主题查哪份文档」加一行指向本文档 | ✅ | +| 修改 | `docs/README.md` §9.2 登记本文档 | ✅ | +| 清理 | 移除遗留 worktree `_v1pin-build` / `_v2oui-build` | ✅ 2026-10-12 已清理(脏改动先留档,见 §2) | +| 验证 | §10 的两个待验证项 | ✅ 见 §10 / §10.1 | + +### 11.1 端到端实测记录(2026-10-12,隔离集成点演练) + +用 `DTU_INTEGRATION_WORKTREE` 把集成点指到一次性工作树、基线用一次性分支, +**全程未触碰真实主线 `v2-on-ui`**: + +| 场景 | 结果 | +|---|---| +| `new` 建树 + 双联接(`.venv` / `simulator/SDL2`) | ✅ 透过联接跑解释器得 `Python 3.12.4` | +| 新工作树里跑 `pre-commit`(共享 hooks) | ✅ `doc-links` 等通过;gitlint 也正常拦下非法邮箱 | +| `list` / `status` | ✅ 落后领先、脏文件数、联接清单、锁归属均正确 | +| `lock` acquire / 冲突拒绝 / 幂等 / 错误持有者拒绝 release | ✅ 期望的 rc 全部命中 | +| `done --dry-run` 无冲突 | ✅ 输出预演结果树,零改动 | +| `done --dry-run` 有冲突 | ✅ 拒绝合并(rc=1),提示到任务分支上解冲突 | +| `done` 完整回收(合并 + 审计 + 重生成 + 清场) | ✅ 合并提交生成合规(标题 40 字符 ≤100、正文 ≤80) | +| 清场安全性 | ✅ 联接摘除前后 `.venv` 文件数 2809 → 2809,解释器完好 | +| `abort` 未加 `--force`(有未合并提交) | ✅ 拒绝(rc=1)并列出将丢弃的内容 | +| `abort --force` | ✅ 联接摘除、工作树移除、分支删除、`.venv` 2809 → 2809 | + +**实测暴露并已修复的 4 个缺陷**: + +1. **锁绑定进程存活 → 形同虚设**:`lock acquire` 是短命进程,一退出锁就被判陈旧、下次必被接管。 + 改为**时效判定**(6 小时 TTL)+ 显式 `--force`,pid 仅作记录。 +2. **`git branch -d` 按环境 HEAD 判「已合并」**:集成点被改走时误判、拒绝删除。 + 改为显式 `merge-base --is-ancestor ` 后再删。 +3. **直调 Win32 建联接被拒**(Win32 4392):改走 `mklink /J`(详见 3.3)。 +4. **`done` 门禁跑错工作树**(最隐蔽的一个):`run_script` 原用 `REPO_ROOT / script` 拼脚本路径, + 而 `REPO_ROOT` 由 `task_worktree.py` 自身位置反推 = **主工作树**。于是 `done` 本意是在集成 + 工作树里校验,实际却跑了主工作树里的同名脚本;该脚本再按它自己的 `__file__` 反推,又去校验 + **主工作树**——校验对象整体错位。 + 证据(回归前/后对照):直接在工作树里跑 `ci_local.py` 报「扫描 .c: 68 / PASS」,经 `done` 调用 + 却报「扫描 .c: 71 / FAIL」并附 `[GAP] App/protocol/iec104/*.c | 缺 uvprojx`(那 3 个文件只以 + **未跟踪**状态存在于主工作树)。修法 = 从传入的 `cwd`(= 集成工作树)解析脚本路径 + (`scripts/ci_local.py` L46 / `scripts/check_registration.py` L52 的 `__file__` 反推是既定约定, + 脚本必须由**目标树**里的那一份执行)。修复后同一条命令的 + `双端注册反向校验 (uvprojx + CMakeLists)` 由 FAIL 转 **Passed**。 + +> 缺陷 4 的教训具有普适性:**任何「按 `__file__` 反推仓库根」的项目脚本,都必须由目标工作树里 +> 的那一份来执行**,否则门禁会静默地校验另一棵树。本 skill 的 `run_script` 已统一按 `cwd` 解析。 + +--- + +## 12. 关联文档 + +| 主题 | 去处 | +|---|---| +| 提交门禁 30 个钩子 | `docs/代码规范/pre-commit使用说明.md` | +| 自动化测试与台架流程 | `docs/测试说明/自动化测试流程.md` | +| 文档写作规范(编码/链接/登记) | `docs/文档规范.md` | +| 工具路径环境变量(换机) | `docs/环境与部署/本地工具路径环境变量方案.md` | +| 待办与排序登记簿 | `docs/任务规划/README.md` | diff --git a/docs/环境与部署/项目部署环境要求.md b/docs/环境与部署/项目部署环境要求.md index 77b0c77c..0b2dab5d 100644 --- a/docs/环境与部署/项目部署环境要求.md +++ b/docs/环境与部署/项目部署环境要求.md @@ -532,7 +532,7 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级 | 开关 | 位置 | 当前值 | 说明 | |---|---|---|---| -| `APP_NET_ENABLE` / `APP_ADC_ENABLE` / `APP_RS485_ENABLE` | `App/app/app_cfg.h` | 全定义 | **未接线占位,不门控任何代码**(P2-12,2026-10-04 核实):三个宏全库零消费点,各模块一律编译进产品;"能否裁剪"取决于产品定义(待处理内容.md §2 阶段 B0),落定后再接入真实门控。**不要把它们当开关** | +| `APP_NET_ENABLE` / `APP_ADC_ENABLE` / `APP_RS485_ENABLE` | `App/app/app_cfg.h` | 全定义 | **未接线占位,不门控任何代码**(P2-12,2026-10-04 核实):三个宏全库零消费点,各模块一律编译进产品;"能否裁剪"取决于产品定义(docs/任务规划/README.md §2 阶段 B0),落定后再接入真实门控。**不要把它们当开关** | | `TEST_SUITE_*` | `test/board/test_config.h` | 全注释(产品构建) | 测试套件**互斥**:一次构建只开一个(CH395F / 存储 / GD5F / RTC);全注释 = 正常产品固件 | --- diff --git a/docs/硬件平台/ADC通道映射说明.md b/docs/硬件平台/ADC通道映射说明.md index 166b385b..709e6277 100644 --- a/docs/硬件平台/ADC通道映射说明.md +++ b/docs/硬件平台/ADC通道映射说明.md @@ -129,10 +129,11 @@ STM32(U17)为 LQFP144 脚号。 | 项 | 现状 | 影响 | |---|---|---| | **通道归属(硬件缺陷)** | 8 路电气量分属两片转换器:UA/UN/IA → TPAFE,UB/UC/IB/IC/IN → STM32 ADC | **V0.1 硬件缺陷**(非有意分配,理由见 §1)→ V0.1 上禁止跨转换器做相位/功率/功率因数/矢量类运算;**V0.2 改版要求 8 路电气量全部走 TPAFE**(登记簿 N-18) | -| STM32 ADC 外设 | **已生成(2026-10-09,CubeMX 6.17 + FW_F4)**:`Src/adc.c`/`Inc/adc.h` 已入库并挂进 `MDK-ARM/*.uvprojx`;`Inc/stm32f4xx_hal_conf.h` 的 `HAL_ADC_MODULE_ENABLED` 已生效;HAL `stm32f4xx_hal_adc.c/_ex.c` + `stm32f4xx_ll_adc.h` 已补入仓库(HAL 原本是裁剪子集);`Src/main.c` 里 `MX_ADC1_Init()` 排在 `MX_IWDG_Init()` 之后。生成配置核对无误:9 通道 / Rank 1~9 / 全部 144 cycles / `ScanConvMode=ENABLE` / `ContinuousConvMode=DISABLE` / `ExternalTrigConv=ADC_SOFTWARE_START` + `EDGE_NONE` / `EOCSelection=ADC_EOC_SINGLE_CONV` / `DataAlign=RIGHT` / 12 位;`HAL_ADC_MspInit()` 已把 PA0~PA7+PC4 配成 `GPIO_MODE_ANALOG` | 硬件通路已打通(`hadc1` 可用);**消费者已落地**——`App/algo/adc_input.c` 的 `stm32_scan()`(L1-3,2026-10-10):`HAL_ADC_Start` + 逐通道 `PollForConversion`/`GetValue`,9 通道一轮 ≈ 67 µs | +| STM32 ADC 外设 | **已生成(2026-10-09,CubeMX 6.17 + FW_F4)**:`Src/adc.c`/`Inc/adc.h` 已入库并挂进 `MDK-ARM/*.uvprojx`;`Inc/stm32f4xx_hal_conf.h` 的 `HAL_ADC_MODULE_ENABLED` 已生效;HAL `stm32f4xx_hal_adc.c/_ex.c` + `stm32f4xx_ll_adc.h` 已补入仓库(HAL 原本是裁剪子集);`Src/main.c` 里 `MX_ADC1_Init()` 排在 `MX_IWDG_Init()` 之后。生成配置核对无误:9 通道 / Rank 1~9 / 全部 144 cycles / `ScanConvMode=ENABLE` / `ContinuousConvMode=DISABLE` / `ExternalTrigConv=ADC_SOFTWARE_START` + `EDGE_NONE` / `EOCSelection=ADC_EOC_SINGLE_CONV` / `DataAlign=RIGHT` / 12 位;`HAL_ADC_MspInit()` 已把 PA0~PA7+PC4 配成 `GPIO_MODE_ANALOG` | 硬件通路已打通(`hadc1` 可用);**消费者已落地**——`App/algo/adc_input.c` 的 `stm32_take()`(L1-3,v2.0,2026-10-10):ADC1 由 **TIM3 TRGO 硬件触发**一轮 9 通道 + **DMA2_Stream4 循环搬运**进乒乓双缓冲,任务侧只消费刚完成的半区(零忙等),9 通道一轮 ≈ 67 µs | +| ADC1 上电自校准 | **已补(2026-10-10)**:`Src/adc.c` 的 `USER CODE BEGIN ADC1_Init 2` 调用 `adc1_calibrate()`(实现同文件 `USER CODE BEGIN 1`,按 RM0090 §13.3.6:清 ADON→等生效→置 ADON→置 CAL→等 CAL 由硬件清零)。⚠ **CubeMX 不生成这一步**,且本仓库裁剪版 HAL **没有** `HAL_ADCEx_Calibration_Start`、裁剪版 CMSIS 头**没有** `ADC_CR2_CAL`(bit3,代码里按 RM 自建 `ADC1_CAL_BIT`)→ 属 L2 手工定制,审计项 `adc1-calibration-call` / `adc1-calibration-impl` 兜底 | 实测有效:校准前后同一拍 STM32 侧 6 路**同向**变化 `UB 898→961`、`UC 978→1038`、`IB 906→922`、`IC 1061→1071`、`IN 1017→1055`、`T 790→846`(+10~+63 LSB ≈ 0.2~1.5 % FS);TPAFE 侧 `UA/UN/IA` 一字不变 → 变化确来自 ADC1 校准。缺这一步不报错,只是**读数带失调** | | ADC 时钟 | CubeMX 的 F4 ADC 面板**有** Clock Prescaler 一项(此前"面板没有该项、固定 DIV2"的判断有误),已选 **PCLK2/4 → ADCCLK = 21 MHz** ✓。面板里 **DIV2 是灰的、选不到**:DB 里 `FadcConstraint Max=36000000`,APB2 = 84 MHz → DIV2 = 42 MHz 超限,该候选值被 `Action="DISABLE"` 禁掉,超规值进不来 | **不设额外防护(2026-10-09 决定)**:约束已由 CubeMX 把关;`.ioc` 虽不保存 `ADC1.ClockPrescaler` 键,重生成也只会回落到约束允许的值。按 144+12 cycles 算 ≈ 7.4 µs/通道、9 通道一轮 ≈ 67 µs | -| `adc_task` | **已实现(B1,2026-10-10;L1 层 v2.3,2026-10-10)**:`App/task/adc_task.c` 每 1 ms 调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 原子快照(8 通道)+ STM32 ADC1 顺序扫描,**经 `adc_input_raw()` 按语义取数**(不再直接索引驱动缓冲);任务在 `Src/freertos.c` 的 `USER CODE` 区创建为 `adcTask`(512 words / Normal) | 数据路径已通(台架 `@test`:`frame_seq` 严格 1000 Hz 递增;`adc1_rc=0` / `adc1_err=0`) | -| TPAFE 采样触发 | **周期 CONVST 已实现(2026-10-10)**:`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 **TIM6 更新中断(1 kHz)** 内调 `tpafe5160_start_conv_irq()`;`app_main_init()` 用 `HAL_TIM_Base_Start_IT(&htim6)` 启动节拍(在 `tpafe5160_init()` 之后) | 连续波形(1 kHz);详见 `子系统设计/ADC采样节拍设计说明.md` | +| `adc_task` | **已实现(B1,2026-10-10;L1 层 v2.3;P1-5 事件驱动消费,2026-10-10)**:`App/task/adc_task.c` 每拍调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 原子快照(8 通道)+ 消费一帧 STM32 ADC1 DMA 乒乓缓冲,**经 `adc_input_raw()` 按语义取数**(不再直接索引驱动缓冲);**消费唤醒由 TIM3 ISR 置位 `ADC_WAKE_FLAG`**(不再 1 ms 轮询);任务在 `Src/freertos.c` 的 `USER CODE` 区创建为 `adcTask`(512 words / **AboveNormal**) | 数据路径已通(台架 `@test`:`frames` 严格 1000 Hz 递增、**`idle=0`**、`adc1_rc=0` / `adc1_err=0`) | +| TPAFE 采样触发 | **周期 CONVST 已实现(2026-10-10)**:`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 **TIM3 更新事件(1 kHz)** 内调 `tpafe5160_start_conv_irq()`;`app_main_init()` 用 `HAL_TIM_Base_Start_IT(&htim3)` 启动节拍(在 `tpafe5160_init()` 之后)。同一 TIM3 更新事件还经 **TRGO** 硬件触发 ADC1 一轮 9 通道(DMA 搬运),并调 `adc_tick_irq_notify()` **唤醒消费者**(P1-5 方案 a,2026-10-10;内含"消费者就绪"门)→ 两片转换器由同一硬件事件同源对齐 | 连续波形(1 kHz);详见 `docs/子系统设计/ADC采样节拍设计说明.md` §10 / §11 | | 前端偏置 | 每路 1k 串联 + 1k 到 `*_GND` + 滤波 C,**无中点偏置** | STM32 侧只能测 **0~3.3 V 单极性**,交流量负半周被削顶;主板侧只做监视可接受,若要算 RMS/功率需硬件补偏置或明确算法口径 | | TPAFE 量程 | 默认 ±5 V(RANGE 接 GND),16 位二进制补码,LSB ≈ 152.59 µV | 用 `tpafe5160_to_voltage()` 换算 | | 取数契约(**2026-10-10 已修**,E-44 / E-45 / E-58 闭环) | 旧的 `get_buf` / `data_ready` / `clear_ready` **已删除**(删除前确认全仓库零调用者):`get_buf` 只给指针、任务再拷 → 被 BUSY EXTI 抢占就是**两帧混合**;`s_ready` 电平标志被 `start_conv_irq` 触发前清 0 → **重触发即丢未消费帧**;`read_all` 从不看 FRSTDATA(宏有定义、代码零引用)→ 芯片死亡时返回 OK + 常量垃圾。现口径:**`tpafe5160_snapshot(dst, count)`**(临界区原子拷贝,返回 1/0/-1,**仅任务上下文**)+ **`tpafe5160_frame_seq()`**(帧序号只增不清)+ **`tpafe5160_frstdata_err()`**(无跳变帧计数);`tpafe5160_init()` 增**首帧自检**——一帧读下来 FRSTDATA 无跳变(芯片不驱动并行总线;板上 R24 4.7k 上拉会把它钉死为高)→ 返回 `-3` + `[ERR]` | 消费者**只能**用 `snapshot()` 取数(`AGENTS.md` 已立为铁律);真机实测开机 `[ADC] Init OK` 现只在自检帧通过后才打印 → 判据在本板成立、非空转 | diff --git a/docs/硬件平台/V0.0_网表详细分析报告.md b/docs/硬件平台/V0.0_网表详细分析报告.md index 50b8489f..8e9e4771 100644 --- a/docs/硬件平台/V0.0_网表详细分析报告.md +++ b/docs/硬件平台/V0.0_网表详细分析报告.md @@ -277,7 +277,7 @@ flowchart LR - 器件为 **SD2506API 模块**(非裸片):内置 32.768k 晶振 + **内置充电电池 + 充电电路**(手册 L11:电池满充常温下掉电走时约 8 个月,电池寿命 5~8 年;自动电源切换:VDD<2.4V 且 VDD **定位**:把参考系统(CLS100 DTU 保护测控装置)的**继电保护链路**在本项目(STM32F4-Base DTU 平台)上分层复现的**总规划**——给出要实现的功能、性能指标、定值参数、平台适配口径与分阶段路线图。 > **参考来源**:`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`。 -> **状态**:规划稿——保护链路本体尚未实现;其前置的 **ADC 采集链路(`adc_task`)已于 2026-10-10 实现并 `@test` 实机闭环**(B1:周期 CONVST + 原子快照消费 + 建任务 + 栈预算 + 台架 1000 Hz 确认)。 +> **相关文档**:`docs/产品需求/产品需求设计文档.md`、`docs/任务规划/README.md`、`docs/内存管理/RAM使用分析与优化方案.md`。 +> **状态**:**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 项主机单测)**;**B5 板端接线完成(2026-10-12)**——`protectTask`(20 ms 判据节拍 + L5-4 采样故障闭锁 + L5-1 定值校验和巡检,IWDG `TASK_WD_PROTECT` bit8,栈 512 words 静态链 872 B / 余 57%),判据结果经 B4 IEC104 遥信(YX15~23/31)上送;**B7 定值持久化 + 全局软压板 + 竞态收口完成(2026-10-12,含台架腿验证)**;**出口记录(L4)与标准源注入验收为下一步**(见 §7)。 --- @@ -37,8 +37,8 @@ | 操作系统 | FreeRTOS V10.3.1(CMSIS-RTOS v2) | | 已实现子系统 | 网络(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()` 取数 + 帧统计;**采样节拍由 TIM6 更新中断(1 kHz)硬件触发**(2026-10-10);STM32 侧 **`App/algo/adc_input.c` 已实现语义映射 + ADC1 9 通道扫描(L1-3/L1-4,2026-10-10)**,`HAL_ADC_Start` 已出现 | -| 保护算法层 | **完全缺失**(无傅氏、无保护元件、无出口、无定值模型) | +| 采样软件 | `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`);**L3 元件框架 + 过流/过负荷/过压/欠压/零序过压 + 定值模型软件侧完成**(`prot_elm.c` + `prot_set.c`,2026-10-11,57 项主机单测);**出口 / SOE 记录仍缺失**(P4) | | 已知硬件缺陷 | **N-18**:8 路电气量分属两片转换器 → V0.1 上禁止跨转换器做相位/功率/矢量运算 | ### 1.3 复现范围界定 @@ -67,7 +67,7 @@ | **B0 产品定义** | 采集是否持续 / 采样率与通道 / 转发目标 / 保护元件清单——决定 B1 与保护层的**规模** | ✅ 已确认(2026-10-10,见 §1.5) | | **B1 ADC 采集链路** | 周期 CONVST + `adc_task` 消费 `tpafe5160_snapshot()` + 建任务 + 栈预算 | ✅ 已实现并 `@test` 实机闭环(2026-10-10;`adcTask` 512 words,台架 1000 Hz) | | **N-18 硬件缺陷** | V0.1 跨转换器无同一时刻性 → 限定 V0.1 可实现的保护范围 | ⚠️ 硬件改版项(V0.2) | -| **栈预算** | 保护任务栈需入 `Src/freertos.c` 并经 `scripts/stack_report.py` 核算 | 🔶 `adcTask` 已核算(静态 504 B / 余 75%;台架实测峰值 496/2048);保护任务为后续 | +| **栈预算** | 保护任务栈需入 `Src/freertos.c` 并经 `scripts/stack_report.py` 核算 | ✅ 两任务均已核算:`adcTask`(**L1-6 后:栈抬至 640 words,静态 1080 B / 台架实测峰值 952/2560 / 余 57%**);`protectTask`(B5,2026-10-12:**512 words,静态链 872 B / 余 1176 B / 57%**,台架水位待 `@test`) | ### 1.5 B0 产品定义(2026-10-10 确认) @@ -200,7 +200,7 @@ | 采样节拍 | 定时器中断(TIMx) | **1 ms** | 最高(ISR) | 产生 CONVST 脉冲;ISR 内只置事件/计数,不做重运算 | | 采集与预处理 | `adcTask` | 1 ms | 高 | `tpafe5160_snapshot()` 取数 → 标定 → 存入采样环 | | 相量计算 | `adcTask` 内联 或 `algoTask` | 1 ms | 高 | 全周傅氏 → 相量双区交换 | -| 保护判据 | `protectTask` | 1 ms | 高 | 各保护元件 Init/Main → 出口 | +| 保护判据 | `protectTask` | 1 ms | 高 | 各保护元件 Init/Main → 出口。**✅ B5 已落地(2026-10-12):V0.1 实为 20 ms / Normal**——元件吃 50 Hz 基波 rms(phasor 每工频周更新),延时精度由 `prot_elm` 的 now_ms 差值法保证、与节拍无关;1 ms/高是参考系统瞬时值元件口径,随 P2 瞬时值元件再评 | | 遥测与显示 | 复用 `uiTask` / 新增 `measTask` | 200 ms | 中 | 谐波方和根、死区/平滑、UI 与上送 | | 记录 | `defaultTask`(已有) | 事件驱动 | 低 | SOE/动作记录落 `sys_log` | @@ -238,15 +238,15 @@ | # | 功能点 | 关键参数 | 完成判据 | 状态 | | --- | --- | --- | --- | --- | -| L1-1 | 周期 CONVST 重触发 | 1000 Hz(与保护节拍同源) | 示波器见 U3 `BUSY` 连续脉冲;`tpafe5160_frame_seq()` 持续递增 | ✅ 2026-10-10:**TIM6 更新中断(1 kHz)硬件触发**(ISR 内 `tpafe5160_start_conv_irq()`;方案 A,零引脚/零驱动改动);台架 `@test` PASS——`seq` 每 10 s 精确 +10000 = 1000.0 Hz、`frst_err=0`(证据 `boot_log_data/20261010_prod_adc_tim6_boot_log.txt`) | +| L1-1 | 周期 CONVST 重触发 | 1000 Hz(与保护节拍同源) | 示波器见 U3 `BUSY` 连续脉冲;`tpafe5160_frame_seq()` 持续递增 | ✅ 2026-10-10:**TIM3 更新事件(1 kHz)硬件触发**(ISR 内 `tpafe5160_start_conv_irq()`;方案 A,零引脚/零驱动改动。⚠ 节拍源**不能**是 TIM6/TIM7——F4 上 ADC1 收不到它们的 TRGO,见 `docs/子系统设计/ADC采样节拍设计说明.md` §11.1);台架 `@test` PASS——`seq` 每 10 s 精确 +10000 = 1000.0 Hz、`frst_err=0`(早期证据 `boot_log_data/20261010_prod_adc_tim6_boot_log.txt`,节拍源当时为 TIM6) | | L1-2 | TPAFE 快照消费 | 8 通道 / 帧 | `adc_task` 每拍调 `tpafe5160_snapshot()`,返回 1/0/-1 有处理 | ✅ B1 已实现并 `@test` 闭环(`adc_task.c`;台架 `idle=0`、`frst_err=0`) | -| L1-3 | STM32 ADC1 启动与扫描 | 9 通道、144 cycles、软件触发 | 全库出现 `HAL_ADC_Start`;9 通道码值随注入变化 | ✅ 2026-10-10:`App/algo/adc_input.c` 的 `stm32_scan()`——`HAL_ADC_Start` + 逐通道 `HAL_ADC_PollForConversion`/`HAL_ADC_GetValue`(EOC_SINGLE_CONV + 无 DMA,可逐通道轮询);台架 `@test` PASS——9 通道全部转换成功、**零超时**(30 s 内 `adc1_rc=0` / `adc1_err=0`,≈27 万次转换)。⚠ 判据"随注入变化"以"逐通道独立读数 + 零超时 + 跨次开机读数不同"替代(台架无信号源,同 §7.1 无示波器口径);**引脚级注入验证(`ADC通道映射说明.md` §8 步骤 2)仍为待做项** | -| L1-4 | 通道语义映射 | 见 `docs/硬件平台/ADC通道映射说明.md`(**按语义取数,不写死下标**) | `UA→buf[6]`、`UN→buf[5]`、`IA→buf[2]`;STM32 侧 `UB→ADC_CHANNEL_14` | ✅ 2026-10-10:`App/algo/adc_input.c` 的 `k_adc_map[]` 语义表(`UA/UN/IA`→TPAFE `buf[6]/[5]/[2]`,`UB/UC/IB/IC/IN/TEMP`→ADC1 `CH14/3/6/1/2/5`)+ `adc_input_raw()` 按语义取数;开机**映射自检**(`adc_input_init()` 校验 STM32 映射通道必落在扫描序列内 → 挡住"映射到不扫描的通道"静默错)通过(`ADC input layer init: OK`)。台架首帧日志 8 路 + 温度全部读出:`UA=11429 UN=11454 IA=11452`(与 TIM6 轮次一致)、`UB=1018 UC=1044 IB=943 IC=1069 IN=1052 T=850`(互异、非饱和;**两次开机读数不同** → 确为实时转换而非常量) | +| L1-3 | STM32 ADC1 启动与扫描 | 9 通道、144 cycles、**TIM3 TRGO 硬件触发 + DMA** | 全库出现 `HAL_ADC_Start_DMA`;9 通道码值随注入变化 | ✅ 2026-10-10(v2.0):`App/algo/adc_input.c` 的 `stm32_take()`——ADC1 由 TIM3 更新事件经 TRGO 触发一轮 9 通道,`HAL_ADC_Start_DMA(&hadc1, buf, 18)` + DMA2_Stream4 循环模式把码值自动搬进**乒乓双缓冲**,任务侧只消费刚完成的半区(**零忙等**,实测单拍 `adcp` 122 µs → **8 µs**);台架 `@test` PASS——`adc1_seq` 每拍 +1、`adc1_err=0`、`adc1_hw=0`。⚠ 判据"随注入变化"以"逐通道独立读数 + 零停摆 + 跨次开机读数不同"替代(台架无信号源,同 §7.1 无示波器口径);**引脚级注入验证(`ADC通道映射说明.md` §8 步骤 2)仍为待做项** | +| L1-4 | 通道语义映射 | 见 `docs/硬件平台/ADC通道映射说明.md`(**按语义取数,不写死下标**) | `UA→buf[6]`、`UN→buf[5]`、`IA→buf[2]`;STM32 侧 `UB→ADC_CHANNEL_14` | ✅ 2026-10-10:`App/algo/adc_input.c` 的 `k_adc_map[]` 语义表(`UA/UN/IA`→TPAFE `buf[6]/[5]/[2]`,`UB/UC/IB/IC/IN/TEMP`→ADC1 `CH14/3/6/1/2/5`)+ `adc_input_raw()` 按语义取数;开机**映射自检**(`adc_input_init()` 校验 STM32 映射通道必落在扫描序列内 → 挡住"映射到不扫描的通道"静默错)通过(`ADC input layer init: OK`)。台架首帧日志 8 路 + 温度全部读出:`UA=11429 UN=11454 IA=11452`(与 TIM3 轮次一致)、`UB=1018 UC=1044 IB=943 IC=1069 IN=1052 T=850`(互异、非饱和;**两次开机读数不同** → 确为实时转换而非常量) | | L1-5 | 通道标定(一次系数 + 零漂) | 每通道 `k`、`b`;系数定点 `2^10` | 注入标准源,读数误差 ≤ 目标精度 | ⬜ | -| L1-6 | 采样有效性校验 | FRSTDATA 跳变、帧序号单调、丢帧计数 | 芯片静默/丢帧被检出并置告警 | ⬜ | +| L1-6 | 采样有效性校验 | FRSTDATA 跳变、帧序号单调、丢帧计数 | 芯片静默/丢帧被检出并置告警 | ✅ 2026-10-11:新增 `App/algo/adc_health.c/h`(纯逻辑判定层:静默 100 ms 门限 / 丢帧边沿 / 序号倒退锁存 / FRSTDATA·HAL 错误·STM32 停摆计数增量)+ `adc_task.c` 告警编排(严重事件与故障上升沿 `DBG_ERROR` + 每类每次开机一次 `SYSLOG_SELFTEST`,恢复 `DBG_INFO`;gap 不告警只计数,心跳增 `tgap`);主机单测 `test/host/adc_health/` **31 用例全绿**(`adc-health-unit-test` 钩子);台架 `@test` PASS 零 `[ERR]`(健康不误报,`boot_log_data/20261011_prod_adc_health_boot_log.txt`)。⚠ 抬栈:判定链 `adc_health_report`→`syslog_post` 回显尾使 adcTask 静态最坏链 832→**1080 B** → 栈 512→**640 words**(余 1480 B / 57%) | | L1-7 | 等周波采样(软件测频 + 调频闭环) | 20 点/周波;测频范围建议 **45~55 Hz**(参考 40.5~60.5) | 频率偏移 ±2 Hz 时幅值误差 ≤ 1% | ⬜ | | L1-8 | 零序通道接入(外部专用 / 自产切换) | 外部 `UN`/`IN`;自产 `3I0 = Ia+Ib+Ic`(V0.2 起) | 零序通道可切换来源,值正确 | ⬜ | -| L1-9 | 二次谐波/五次谐波提取入口 | k=2(涌流)、k=5(选线) | 相量层可产出对应谐波相量 | ⬜ | +| L1-9 | 二次谐波/五次谐波提取入口 | k=2(涌流)、k=5(选线) | 相量层可产出对应谐波相量 | ✅ 2026-10-11:`phasor.c` 实例表 `C04-IA-F2`(k=2)/ `C05-IA-F5`(k=5)随每拍重算产出,台架心跳日志 `phasor(hb) C04/C05` 行可见(`boot_log_data/20261011_prod_phasor_boot_log.txt`);谐波**抑制/提取正确性**已由主机单测覆盖(`test/host/phasor/`) | > ⚠️ **N-18 约束**:L1-4 中 `UA/UN/IA` 在 TPAFE、`UB/UC/IB/IC/IN` 在 STM32 ADC。V0.1 上 L1 只做**逐通道独立监视**,不得假设同族量同刻(`UA` 与 `UB` 无共同采样触发)。 @@ -254,17 +254,21 @@ | # | 功能点 | 关键参数 | 完成判据 | 状态 | | --- | --- | --- | --- | --- | -| L2-1 | 全周傅氏系数表 | `sin(2πk/N)·(√2/N)`、`cos(2πk/N)·(√2/N)`,N=20 | 系数表与 `docs/算法/傅氏算法详解.md` §2.3 一致 | ⬜ | -| L2-2 | 基波相量实例 | 4 个实例:C01 基波 I/U、C02 零序基波、C03 二次谐波、C04 五次谐波 | 每实例输出 `{fRe, fIm, fAm, fAmSQ}` | ⬜ | -| L2-3 | 相量有效值输出 | `fAmSQ = √(fRe²+fIm²)` 直接为**有效值**(系数已折算) | 标准源读数 = 设定有效值 | ⬜ | -| L2-4 | 相位输出 | `φ = atan2(fIm, fRe)` | 相位误差 ≤ 目标值 | ⬜ | -| L2-5 | 线/负序/零序合成 | 线电压 `Uab=Ua−Ub` 等;负序 `3U2`/`3I2`;零序 `3U0`/`3I0` | 合成相量可复用于保护元件(**V0.1 仅同转换器内可合成**,见 §1.6) | ⬜ | -| L2-6 | 每 1 ms 滑动窗刷新 | 每来新点,窗滑动一格、20 点重算(暴力法) | 相量每拍更新 | ⬜ | -| L2-7 | 双区指针原子交换 | 计算区/使用区切换 | 保护元件读使用区,无撕裂 | ⬜ | +| L2-1 | 全周傅氏系数表 | `sin(2πk/N)·(√2/N)`、`cos(2πk/N)·(√2/N)`,N=20 | 系数表与 `docs/算法/傅氏算法详解.md` §2.3 一致 | ✅ 2026-10-11:`phasor.c` 离线系数表**预乘 √2/N**;主机单测逐项复核(`test/host/phasor/` 系数表用例) | +| L2-2 | 基波相量实例 | **5 个实例**(参考项目 4 个,见下方注):`C01-IA-F1` / `C02-UA-F1` / `C03-UN-F1` / `C04-IA-F2` / `C05-IA-F5` | 每实例输出 `{fRe, fIm, fAm, fAmSQ}` | ✅ 2026-10-11:`k_inst[]` 数据驱动 5 实例;台架开机即产出 `phasor(first)` 5 行(`boot_log_data/20261011_prod_phasor_boot_log.txt`) | +| L2-3 | 相量有效值输出 | `fAmSQ = √(fRe²+fIm²)` 直接为**有效值**(系数已折算) | 标准源读数 = 设定有效值 | ✅ 2026-10-11(链路级):`fAmSQ` 口径实现 + 单测合成正弦幅值误差 ≤1%;**"标准源读数"判据在无信号源台架不可达**(同 §7.1),待 L1-5 标定后复核 | +| L2-4 | 相位输出 | `φ = atan2(fIm, fRe)` | 相位误差 ≤ 目标值 | ✅ 2026-10-11(链路级):`fRe/fIm` 常驻双区、每拍产出,φ 由消费侧 `atan2f` 现算(不常驻结构体,见 `App/algo/README.md` §3.3);单测覆盖相位;**目标值待产品定义 + 标准源复核** | +| L2-5 | 线/负序/零序合成 | 线电压 `Uab=Ua−Ub` 等;负序 `3U2`/`3I2`;零序 `3U0`/`3I0` | 合成相量可复用于保护元件(**V0.1 仅同转换器内可合成**,见 §1.6) | ⬜ **V0.1 不适用**:跨转换器合成被 N-18 禁止;零序走**外部专用通道** `UN`/`IN`(§6.4),无需自产 → 随 V0.2 启用 | +| L2-6 | 每 1 ms 滑动窗刷新 | 每来新点,窗滑动一格、20 点重算(暴力法) | 相量每拍更新 | ✅ 2026-10-11:`sample_ring` 40 点环 + `phasor_run()` 每拍整窗重算(仅 `r==1` 新帧压环,P0 修复);台架 `ring` 随 `frames` 同步递增、`idle=0` | +| L2-7 | 双区指针原子交换 | 计算区/使用区切换 | 保护元件读使用区,无撕裂 | ✅ 2026-10-11:`s_zone[2][5]` + 单字节 `volatile` 切换(Cortex-M 原子);单测覆盖双区交换;为独立 L3 任务预留无锁读 | | L2-8 | 遥测谐波轨(可选) | k=1~9,方和根 `√(ΣHk²)` | 与基波轨并行,供 UI/上送 | ⬜ | > **双轨有效值**:保护要"基波有效值 + 相位"(抗衰减直流、抗谐波误动);计量/遥测要"真有效值"(谐波方和根)。两者**不可混用**——这是参考系统 `CN_ANA_VEC_SQRT=FALSE` 与遥测谐波轨并存的根本原因。 +> **L2-2 实例数 4 → 5 的来历**:参考项目(`APP/CLS100_Dsp/InstCalCell.c`)为 **4 个实例**——C01 基波 I/U、C02 零序基波、C03 二次谐波、C04 五次谐波。本项目按 V0.1 **有效通道**(`UA`/`UN`/`IA`,见 §1.6:`UB/UC/IB/IC/IN` 无中点偏置 → 削顶不可用)展开:C01 的"I/U"拆为两行(`C01-IA-F1` 电流 / `C02-UA-F1` 电压),其余顺延为 `C03-UN-F1` 零序、`C04-IA-F2` 二次、`C05-IA-F5` 五次。实现见 `App/algo/phasor.c` 的 `k_inst[]`;判据按 **5** 个实例断言(见 `docs/测试说明/相量层单元测试说明.md` §2.6)。 + +> **本表状态列口径**(2026-10-11 更新):L2-1~L2-4 / L2-6 / L2-7 已**实现 + 主机单测(57 用例)+ 台架 `@test` 闭环**——闭环命令 `run_boot_test.py --required "...;phasor(first);phasor(hb)" --require-order "App init OK>phasor(first)>phasor(hb)"`,证据 `boot_log_data/20261011_prod_phasor_boot_log.txt`(`fw build` 身份戳匹配、回读一致、锚点 ×1、零 `[ERR]`)。⚠ **幅值/相位精度判据(标准源)在无信号源台架上不可达**(同 §7.1 无示波器口径),且 L1-5 标定未实现 → 现读数为**原始码值**,只证明链路通;精度复核排在标定之后。L2-5 随 V0.2(N-18),L2-8 可选项未做。 + ### 3.3 L3 保护元件 采用参考系统的**统一接口模式**:每元件一个 `tagPRS_XXX` 配置结构(定值号、模拟通道号、KI/KO、标志位、校验和)+ `Init` / `Main`(判据)/ 出口函数。 @@ -273,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 到点/滞回保持/返回释放;**板端接线已落地(B5,2026-10-12:protectTask 20 ms 节拍,动作经 104 YX16~18 上送)**;出口(L4)待做 | +| L3-2 | 三段式零序过流 | 零序电流 > 定值,经延时出口 | Ⅰ/Ⅱ/Ⅲ 段零序电流 + 延时 | ⬜(N-18:`IN` 走 STM32,V0.2 使能;框架已就绪,加实例表一行即可) | +| L3-3 | 过负荷 | 相电流 > 定值,告警/跳闸可整定 | 过负荷电流 + 延时 | 🔶 同 L3-1(`OL-IA` 实例,默认告警不跳闸;板端接线随 B5 落地,YX23 上送) | +| L3-4 | 过压 / 欠压 | 相/线电压越限,经延时出口 | 过压定值、失压定值 + 延时 | 🔶 同 L3-1(`OV-UA`/`UV-UA`,欠量方向单测覆盖;沿用参考告警段 0x8027/0x8029) | +| L3-5 | 过频 / 低频 | 频率越限,经延时出口 | 过频、低频定值 + 延时 | ⬜(随频率测量算法 P2;框架已就绪) | +| L3-6 | 零序过压 | 零序电压 > 定值 | 零序过压定值 + 延时 | 🔶 同 L3-1(`3U0-UN` 实例,外部零序电压通道,不受 N-18 限制) | **扩展集(P2/P3)** @@ -316,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` 模式);**B7(2026-10-12)竞态收口**:104 定值写改 `prot_set_set_sealed`(影子区写+封存后单字交换)+ selfcheck seqlock 式复核(提交撞见 = 本轮跳过),B5 登记的 µs 级误锁窗口消除;定值持久化 `/prot_set.bin`(blob 带 sum32 正/反码对,整表原子装载);元件级 tagPRS 常量表校验仍待补 | +| L5-2 | 采样通道自检 | 通道码值越界/静默检出 | 🔶 静默/丢帧/序号倒退/错误增量已由 L1-6 `adc_health` 检出(2026-10-11);码值越界待做 | | L5-3 | 硬件自检 | 出口回路、电源、时钟 | ⬜ | -| L5-4 | 自检失败闭锁 | 任一项失败 → 闭锁相关保护并告警 | ⬜ | +| L5-4 | 自检失败闭锁 | 任一项失败 → 闭锁相关保护并告警 | 🔶 定值闭锁 → 全元件复位已联动(`prot_elm_eval` 读 `prot_set_locked()`,单测覆盖);**采样故障闭锁已落地(B5,2026-10-12)**:protectTask 每拍读 `adc_health_flags()` 电平位(TPAFE/STM32 stall + 序号倒退)→ 命中即 `prot_elm_init()` 复位 + 本拍不判据,自愈后自动恢复;锁存类事件位(FRSTDATA/HW_ERR)不构成"当前测量不可信"→ 不闭锁,如实登记 | --- @@ -346,17 +350,17 @@ | 定值折算 | 定点系数 `2^10` | 与参考一致 | | 遥测微调 | ±10%,0.1% 步长(TLV10) | 参考口径 | -> **误差来源与预算**(参考《ADC采集至数据使用全流程详解》§12):采样量化(TPAFE LSB ≈ 152.6 µV)、通道标定残差、窗长偏离整周波(频偏)、系数定点舍入。前三项决定可达精度,最后一项可忽略。 +> **误差来源与预算**(参考《ADC采集至数据使用全流程详解》[计划] §12):采样量化(TPAFE LSB ≈ 152.6 µV)、通道标定残差、窗长偏离整周波(频偏)、系数定点舍入。前三项决定可达精度,最后一项可忽略。 ### 4.3 算力与内存预算 | 项 | 估算 | 说明 | | --- | --- | --- | | 傅氏乘加 | 20 点 × 2(sin/cos)× 相量数 / 拍 | 参考用暴力法(CPU 余量足够);本项目 168 MHz + FPU 需实测 | -| 相量区 RAM | `tVectorRly[2][N] × sizeof(相量)` | 双区 + 4 实例 | +| 相量区 RAM | `tVectorRly[2][N] × sizeof(相量)` | 双区 + **5 实例**(本项目;参考项目 4 实例) | | 采样缓冲 RAM | 40 点/通道 × 通道数 × 字宽 | 双缓冲 2 周波 | | 定值区 RAM | 元件数 × 定值项 × 字宽 | 可由 cfg_file 载入 | -| 任务栈 | `adcTask` / `protectTask` 待核算 | 建任务前后各跑 `scripts/stack_report.py` | +| 任务栈 | `adcTask` ✅ / `protectTask` ✅(B5:512 words,静态链 872 B / 余 57%) | 建任务前后各跑 `scripts/stack_report.py` | > ⚠️ **改内存相关代码后必须刷新快照**:`@build` 出 `.map` 后跑 `scripts/check_mem_numbers.py --write`,同步 `docs/内存管理/RAM使用分析与优化方案.md` §2.1(`mem-numbers` 钩子会因体积变化 FAIL)。 @@ -404,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 馈线典型量级拟定),待对照参考出厂表逐项修订。 --- @@ -459,11 +465,11 @@ | 阶段 | 名称 | 内容 | 前置 | 里程碑 | | --- | --- | --- | --- | --- | -| **P0** | 采样与相量底座 | B1(周期 CONVST + `adcTask` 消费 + 建任务 + 栈预算)+ L1 + L2 | B0 + B1 ✅ | 每 1 ms 产出正确的基波相量 | -| **P1** | 基础保护元件 | **V0.1 有效子集**:过流(A 相,`IA`)、过负荷(A 相)+ 定值模型;B/C 相过流**建接口不使能** | P0 | 标准源注入 A 相可正确动作 | -| **P2** | 电压/频率类保护 | 过压/欠压(`UA`)、过频/低频、零序过压(`UN`) | P0 | 电压/频率越限正确动作 | +| **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 相可正确动作 —— 🔶 **软件侧 2026-10-11 完成**(`prot_set` 定值模型 30 用例 + `prot_elm` 元件框架/过流三段/过负荷 27 用例,主机全绿);**板端接线已落地(B5,2026-10-12:protectTask + IWDG bit8 + 栈核算 + 104 YX 上送)**;标准源注入验收待做 | +| **P2** | 电压/频率类保护 | 过压/欠压(`UA`)、过频/低频、零序过压(`UN`) | P0 | 电压/频率越限正确动作 —— 🔶 过压/欠压/零序过压随 P1 同批主机完成(同框架实例);**过频/低频待频率测量算法** | | **P3** | 异常检测与闭锁 | L5 自检闭锁、采样通道自检;三相类(涌流/PT·CT 断线/母线接地)**建接口不使能** | P1/P2 | 自检失败闭锁;未使能元件不误动 | -| **P4** | 出口、记录与定值集成 | L4 出口与记录、§5 定值模型落 cfg_file、UI 显示、上送 | P1 | 动作出口 + SOE 记录 + UI 显示闭环 | +| **P4** | 出口、记录与定值集成 | L4 出口与记录、§5 定值模型落 cfg_file、UI 显示、上送 | P1 | 动作出口 + SOE 记录 + UI 显示闭环(**定值持久化 + 软压板 + 竞态收口 = B7 完成 2026-10-12,台架腿已验 ✅**) | | **P5** | 方向/功率类(延后) | L3-13 方向元件等 | V0.2 硬件 / 子板计量 | 跨转换器相位一致后实现 | > ⚠️ **V0.1 硬件裁剪**(2026-10-10,详见 §1.6):V0.1 上只有 TPAFE 三路(`UA`/`UN`/`IA`)测量有效。故 **P0 骨架全做**(与通道无关),**P1/P2 只做有效通道子集**——A 相过流、A 相过负荷、过压/欠压、零序过压、过频/低频;**三相过流(B/C 相)、零序过流、方向/功率类、PT/CT 断线**依赖 V0.2 硬件(N-18 修复 + 中点偏置),本期**建接口不使能**。 @@ -534,7 +540,7 @@ B0 产品定义 ✅(2026-10-10 已确认,见 §1.5) | R5 | **STM32 侧无中点偏置** | 交流量负半周削顶,无法算 RMS | V0.1 只做监视;V0.2 补偏置 | | R6 | **栈预算** | 新任务栈溢出 | 建任务前后跑 `scripts/stack_report.py`;纳入看门狗 | | R7 | **出口硬件未确认** | 跳闸出口无法验证 | 确认出口继电器/回读电路;无硬件时先做逻辑级验证 | -| R8 | **TPAFE 自动扫描模式未知** | 影响采样实现方式 | 查手册确认(登记簿 §4 待确认项) | +| R8 | **TPAFE 自动扫描模式未知** | 影响采样实现方式 | 查手册确认(`docs/任务规划/基线与待确认.md` §1 待确认项) | --- @@ -579,7 +585,6 @@ B0 产品定义 ✅(2026-10-10 已确认,见 §1.5) 2. 出口硬件形态(继电器/回读)——依硬件(R7)。 3. 等周波调频范围与精度目标——依产品定义(R4)。 4. 是否保留遥测谐波轨(k=1~9)——依 UI/上送需求。 -5. 遥测/遥信/遥控的 104 上送实现范围与排期——依产品定义。 --- @@ -595,3 +600,11 @@ B0 产品定义 ✅(2026-10-10 已确认,见 §1.5) | 2026-10-10 | **B1 落地:ADC 采集链路实现** | `App/task/adc_task.c` 由空桩转为真实消费者——每 1 ms 触发 `tpafe5160_start_conv_irq()` + `tpafe5160_snapshot()` 原子取数 + 帧统计 + 每 10 s 心跳日志;`Src/freertos.c` 手工创建 `adcTask`(512 words / Normal,落 `USER CODE` 保护区)+ 纳入 IWDG 必需掩码(`TASK_WD_ADC`,`0x37`→`0x77`)。§1.4 采样软件行、§1.4 依赖表 B1 行、§3.2 任务表(L1-1/L1-2)、§4 栈预算行、§7.1 P0 行同步更新。⚠ 本阶段为**任务周期软触发**(`osDelayUntil`);硬件定时器 1 kHz 触发为后续。同日 `@test` 实机闭环 PASS(`frame_seq` 严格 1000 Hz 递增、`idle=0`、`frst_err=0`、栈峰值 496/2048) | | 2026-10-10 | **1 kHz 硬件定时器触发(TIM6)** | 采样节拍由任务周期软触发改为 **TIM6 更新中断(1 kHz)**:`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 加 TIM6 分支触发 `tpafe5160_start_conv_irq()`(`USER CODE` 区);`app_main.c` 用 `HAL_TIM_Base_Start_IT(&htim6)` 替换单次触发(在 `tpafe5160_init()` 之后);`adc_task.c` 退化为**纯消费者**(1 ms 轮询仅作消费节奏)。§1.2 采样软件行、§3.2 L1-1 同步。详见 `docs/子系统设计/ADC采样节拍设计说明.md` | | 2026-10-10 | **L1-3/L1-4 落地:STM32 ADC1 扫描 + 语义映射** | 新增 `App/algo/adc_input.c/h`(L1 采集层):① `k_adc_map[]` **语义通道表**(`UA/UN/IA`→TPAFE `buf[6]/[5]/[2]`,`UB/UC/IB/IC/IN/TEMP`→ADC1 `CH14/3/6/1/2/5`)+ `adc_input_desc()`/`adc_input_raw()` **按语义取数**;② `stm32_scan()` STM32 ADC1 9 通道软件触发顺序扫描(`HAL_ADC_Start` + 逐通道 `PollForConversion`/`GetValue`);③ `adc_input_poll()` 统一"TPAFE 快照 + STM32 扫描"。`adc_task.c` 取数改走本层(首帧日志补全 8 路 + 温度、心跳增 `adc1_rc`/`adc1_err`);`app_main.c` 在 `tpafe5160_init()` 后调 `adc_input_init()`(含**映射自检**:STM32 映射通道必落在扫描序列内,失败记 `SYSLOG_SELFTEST`);Keil 工程挂 `adc_input.c` 并加 `../App/algo` 包含路径。§1.2 采样软件行、§3.1 L1-3/L1-4 同步。`@build` 0 错 0 警;台架 `@test` PASS(`ADC input layer init: OK`、`adc1_rc=0`/`adc1_err=0`、`seq` 仍 1000.0 Hz、`adc` 栈峰值 528→584 B、回读一致、零 `[ERR]`/HardFault),证据 `测试说明/boot_log_data/20261010_prod_adc_l1_boot_log.txt` | +| 2026-10-10 | **L2 相量层落地 + L2-2 实例数口径对齐** | 新增 `App/algo/sample_ring.c/h`(L2 输入:按语义通道保存 2 周波 = 40 点环缓冲,整帧写后统一推进写指针)与 `App/algo/phasor.c/h`(L2 核心:全周傅氏单频 DFT,N = 20;系数表**离线预乘 √2/N** → `fAmSQ` 直接为**有效值**;计算区/使用区**双区原子切换**;实例表 `k_inst[]` 数据驱动);主机侧单测 `test/host/phasor/` **57 用例全绿**(`phasor-unit-test` 钩子)。**§3.2 L2-2 行口径对齐**:参考项目为 **4 个实例**,本项目按 V0.1 有效通道展开为 **5 个**(`C01-IA-F1`/`C02-UA-F1`/`C03-UN-F1`/`C04-IA-F2`/`C05-IA-F5`),已加注说明 4 → 5 的来历;§4.3 相量区 RAM 行同步为 5 实例。⚠ §3.2 的 L2 各点状态**仍标 ⬜**——实现与主机单测已完成,但**台架 `@test` 闭环未做**,且 L2-3 的完成判据(标准源读数 = 设定有效值)在无信号源台架上不可达,故不置 ✅(口径见 §3.2 表下注) | +| 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(`docs/内存管理/RAM使用分析与优化方案.md` §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 任务表 / `docs/内存管理/RAM使用分析与优化方案.md` §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 落实进度注 | +| 2026-10-12 | **B5 板端接线:protectTask 立项落地** | 新增 `App/task/protect_task.c/h`(**纯编排**,判据仍在 `App/algo/`):20 ms 节拍三件事——① **L5-4 采样故障闭锁**:`adc_health_flags()` 电平位(TPAFE/STM32 stall + 序号倒退)命中 → `prot_elm_init()` 复位 + 本拍不 eval(stall 自愈自动恢复;锁存类事件位不闭锁——非"当前测量不可信");② **L5-1 定值校验和巡检**(每拍 `prot_set_selfcheck()`,31 项 sum32 开销可忽略;⚠ 与 104 定值写的竞态窗口 µs 级、同次写 seal 即清锁,如实登记,P4 定值持久化收口);③ `prot_elm_run()` + 动作位图边沿日志(`[PROT]`,V0.1 无出口 → 动作 = 遥信 + 日志)。**线程契约收口**:`prot_elm_request_reset()`(单字标志,任意上下文)——104 YK11 在 iec104Task 落请求、下一拍 eval 头部消费,杜绝与 protectTask eval 竞态(`iec104_pts.c` 改走此口);定值/元件初始化移入 `app_main_init`(内核前,phasor_init 之后)。**门禁(E-59 同款)**:`TASK_WD_PROTECT = 8` 入 `wd_required_mask()` 两分支(产品 `0xF7`→`0x1F7`、HW 层 `0xF5`→`0x1F5`,看门狗单测 39 断言同步);栈 **512 words**,`stack_report.py` 实测静态最坏链 **872 B / 余 1176 B / 57%**;**FreeRTOS 堆 48→56 KB**(`.ioc` L1 参数 + `FreeRTOSConfig.h` 同步,CCM 75.3%→87.9% < 92% 门禁,`RAM预算台账.md` §6 登记配额 48→56 KB)。任务手写于 `Src/freertos.c` USER CODE 区(Normal,V0.1 无出口不需 AboveNormal;§2.2 表已注"1 ms/高"为 P2 瞬时值口径)。单测 +1(TC-PE-010 复归请求:不当拍生效、下一拍消费,滞回保持区可区分复位与否)→ prot_elm 28 用例全绿。`@build` 0 错 0 警(Code=204348,ZI +8,196 = 堆 +8 KB);台架 `@test`(PROT 锚点 + `[STACK] protect` 水位 + 104 YX 联动)待做。§3.3 L3-1/3 注板端落地、§3.5 L5-4 置 ✅、§7 P1 里程碑更新 | +| 2026-10-12 | **B7 定值持久化 + 全局软压板 + 竞态收口(W1 任务卡)** | **定值模型增项** `PS_GLOBAL_EN`(全局软压板,枚举尾追加,编号不回收);**原子写**:`prot_set_set_sealed()` 影子区拷贝→改→重封→**单字交换活跃区**(双缓冲 +124 B 静态),`prot_set_selfcheck()` 改 seqlock 式复核(封存和两读不一致 = 提交撞见 → 本轮跳过)——B5 登记的"104 写 vs 巡检"µs 级误锁窗口就此消除(§3.5 L5-1 注收口)。**持久化**:新增 `App/cfg/prot_set_store.c/h`(双端注册 + 模拟器豁免点名)——blob = magic`PSET`+版本+计数+值区+sum32 正/反码对(144 B),`/prot_set.bin` 走 `cfg_store_write_atomic`(临时文件+rename,掉电安全);**写路径** = C_SE/YK12 执行段置脏 → iec104Task `dirty_peek` 投递 `SYS_EVT_PROT_SET_SAVE`(事件位 bit7)→ defaultTask(配置引擎属主)落盘,写失败脏回置 = 下轮重试;**装载** = storage_bring_up 在 STORAGE_READY 前整表原子 parse(损坏/越界/版本换代 → 拒绝保出厂默认 + 自检留痕 `prot set corrupt`;文件缺失 = 首启正常静默)。**YK12 转真**:`iec104_pts.c` 两段式(选择段校验 DPC、执行段 set_sealed),protectTask 下一拍读退出态 → 复位元件 + 停判据(`global plate OUT/IN` 边沿日志);本地定值项 `comm_addr=0` 显式不可寻址(防 IOA=0 误命中)。单测:prot_set 63 断言(+33:软压板/原子写/脏握手/blob 往返/坏 magic/截断/坏版本/**伪造自洽越界**对抗样本)、iec104 94 断言(+7:YK12 两段转真/置脏/本地项 C_RD EVI)。`@build` 0 错 0 警(Code=205500 +1152 B,ZI +280 = 双缓冲+blob 缓冲,`ram_baseline` 已刷)。**台架腿完成(2026-10-12)**:`@test` PASS + PC 主站 `test/host/iec104_master.py --b7-pre` **21/21**(YK12 两段 OFF→0.05 A 低定值不动作 / ON→YX15·16 即动作,C_SE→C_RD 回读)、`--b7-post` **13/13**(**压板退出态跨复位保持**:开机日志 `global plate OUT -> protection disabled` + 低定值仍不动作、重启后 YK12 ON 联动在位)、`--b7-mark/--b7-check` **4/4+4/4**(**非默认定值 7.5 跨复位保持**,C_RD=7.500,与出厂默认可区分);开机日志两次 `[CFG] prot set loaded (/prot_set.bin)`;证据 `docs/测试说明/boot_log_data/`(20261012_B7_* 两份) | +| 2026-10-12 | **B6 台架 `@test` 闭环复测(B4/B5 验收窗口,W1 任务卡)** | 首跑(HEAD `d3107f3`)**FAIL**:`protect task start` 锚点行字节级交错损坏(`st[art: cycle=220ms elm=70`,两次开机同形复现)= protectTask 启动行违反控制台写者注册前门控纪律(B5 台架腿未跑故未暴露,机理与实证见 `docs/子系统设计/控制台单写者日志设计说明.md` §11 2026-10-12 行);其余 8 Required / 零 `[ERR]` / 锚点×1 / 回读一致全过。**修复** = 启动锚点行改 `dbg_console_writer_ready()` 首拍补打(adcTask 首帧门控同款,+16 B)。**复测 PASS**(B7 `1fe163b`+fix):锚点行完好(+577 ms,attach 后);**首采水位** `protect` 828/2048(free_min 1220,59%)、`iec104` 424/2560(83%)、新 56 KB 堆 free=17936 **min_ever=10000**(>3072 告警线零告警,**N-33 抬栈 min_ever 背书就绪**);采样闭锁不误报。**PC 主站往返**:新增 `test/host/iec104_master.py`(纯标准库主站客户端)STARTDT→总召四组+actterm→C_SE 两段写临时压低 OC1 定值**确定性**触发 YX15·16 变位(COT=3)→恢复复归→YK11 两段正答→C_RD 读回 10.000,**18/18 PASS**(`d3107f3` 与 B7+fix 两版固件各一轮),串口侧 `[I104] accepted`/`[PROT] elm OC1-IA` 边沿/YK11 复位→UV-UA +10 s 重返联一致。证据 `docs/测试说明/boot_log_data/20261012_B6_*`(含缺陷复现 MISS 快照)。B5 行"台架 `@test` 待做"遗留**闭环**;B7 台架腿的 C_SE 写路径已随本窗验证(断电重启保持 + YK12→YX 仍待窗) | diff --git a/docs/网络/BSD_Socket_API_使用指南.md b/docs/网络/BSD_Socket_API_使用指南.md index 9d10d066..8b132ba2 100644 --- a/docs/网络/BSD_Socket_API_使用指南.md +++ b/docs/网络/BSD_Socket_API_使用指南.md @@ -175,7 +175,7 @@ int net_close_sock(net_sock_t *p_sock); - **`netTask` 不可向自身发消息**:`netTask` 中直接调 `net_send()` 会死锁(等待自己处理消息),必须在 `netTask` 中使用 `net_send_sock()` 等内部版本 - **阻塞 recv 不再独占 netTask(L3 pending 化,2026-09-22)**:无数据时公共 `net_recv()` / `net_recv_to()` **登记挂起请求后返回**(每 fd 至多一个,禁止覆盖——覆盖即丢失前调用方通知 = 永久挂死);完成由 `net_pending_tick()` 按主循环节拍检查(①数据 ②到点 ③断连,看门狗 1s)并经任务通知唤醒调用方——等待期间调用方在自己上下文睡通知,netTask 零占用(消除 E-02 头阻塞)。副作用:`buf` 指针须保持有效至返回;close / 网络重建 / 陈旧重同步会**显式取消并通知**(`NET_ERR_CONNRESET` 等),调用方永不挂死 - **队列阻塞语义**:只有 `osMessageQueuePut` 失败(8 长度固定队列,极端情形)时公共 API 阻塞;recv 挂起等待**不计入**队列占用(完成前队列中无消息滞留) -- **哪些操作要求 netTask 已运行**:`net_listen()` / `net_accept()` / `net_connect()` / `net_send()` / `net_recv()` / `net_close()` / `net_relisten()` / `net_resync_listen()` 全经消息队列 → 只能在就绪屏障后的任务调用;`net_socket()` / `net_bind()` 为直接调用,但同样建议屏障后 +- **哪些操作要求 netTask 已运行**:`net_listen()` / `net_accept()` / `net_connect()` / `net_send()` / `net_recv()` / `net_close()` / `net_relisten()` / `net_resync_listen()` 全经消息队列 → 只能在就绪屏障后的任务调用;`net_socket()` / `net_bind()` 为直接调用,但同样建议屏障后;**`net_can_accept()` / `net_can_read()` 是纯 RAM 读控制块**(§10.1),不入队、不等回执,**任意任务可安全周期调用**(用途 = 门控,把"无谓的 RPC"降为 0) ### 3.6 芯片/本层状态对账与重监听自愈(Trap 09 / 22 / 23) @@ -873,6 +873,23 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen); > 单连接模式:监听 Socket 在 CONNECT 后**自身即数据通道**(§5.1)。`net_accept` 失败时若状态为 CLOSED 且 `auto_relisten` 开启,内部触发**非阻塞**重监听(失败交重试轮询,§3.6)。中断丢失也不会让监听死亡——对账在 200ms 内修正"本层 LISTENING 而芯片监听消失"等失同步。 +#### net_can_accept / net_can_read - 就绪门控(**纯 RAM 读,不走消息队列**) + +```c +int net_can_accept(int sockfd); /* 1 = 有连接可接受;0 = 无 */ +int net_can_read(int sockfd); /* 1 = 应发一次 recv;0 = 可跳过本轮 */ +``` + +| 项 | 说明 | +|------|------| +| 代价 | **零 RPC**:只读 Socket 控制块(`net_get_sock()`),不入队、不等 netTask 回执 → 任意任务上下文可安全周期调用,**不被 netTask 忙碌程度拖住** | +| `net_can_accept` 判据 | `state == ESTABLISHED`,与 `net_accept_locked()` 严格等价(单连接模型:监听 Socket 自身在 CONNECT 后转数据通道) | +| `net_can_read` 判据 | `ESTABLISHED` → 看 `readable`;**非 `ESTABLISHED` 一律返 1**(必须让上层 recv 一次拿到 EOF/错误并复位自身状态,门掉它会把"连接已消失"永久藏住);描述符失效也返 1(让 recv 报 BADF 走既有错误分支) | +| 用途 | **门控**:无活时跳过 `net_accept()`/`net_recv()`——两者内部都是 `osMessageQueuePut(osWaitForever)` + `xTaskNotifyWait(portMAX_DELAY)`,**均无上界**(队列深 8 且与 FTP/timeSync 共享) | +| ⚠ 不可作为唯一判据 | `readable` 只由 RECV_OK 中断置位,而 §3.6 的三类对账**不含**"芯片 RX 非空但 `readable=0`" → 中断丢失时 `net_can_read()` 恒返 0。**调用方必须保留低频兜底 recv**(`ui_net_sink.c` 每 32 轮无条件收一次,教训见 `docs/子系统设计/看门狗设计说明.md` §11.5 第 1 条) | +| 竞态 | netTask 是唯一写者,单字读;最坏"晚一轮发现"(netTask 每 1 ms 就 `net_poll()`)或"扑空"(RPC 返回 `NET_ERR_WOULDBLOCK`),均无副作用 | +| 模拟器端 | `net_socket_sim.c` 同名实现**恒返回 1**——Winsock 本就非阻塞、无 RPC 成本,恒 1 使门控在模拟器上逐位等价于改前行为 | + #### net_connect - 连接服务器 ```c @@ -1395,6 +1412,7 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po | 日期 | 变更 | |------|------| +| 2026-10-10 | 新增 **`net_can_accept()` / `net_can_read()` 就绪门控 API**(§10.1):纯 RAM 读控制块、**不走消息队列、不等 netTask 回执**,供实时性敏感任务在发 RPC 前判断"有没有活"(动因 = `ui_net_sink_poll()` 每 10 ms 一次 `net_accept` 把 uiTask 拖成最坏 ≈50 ms;原拟登记 N-19 / C7,当日结案不入登记簿;设计与实测见 `docs/子系统设计/看门狗设计说明.md` §11(原分析文档已并入该节));§3.5 补"netTask 运行要求"例外条;⚠ 记录两条使用约束——**不可作为唯一判据**(`readable` 只由 RECV_OK 中断置位,须保留低频兜底 recv)与**模拟器后端恒返 1**(`net_socket_sim.c`,Winsock 无 RPC 成本) | | 2026-09-23 | 同步 L3 pending 化、D10 非阻塞重监听、Trap 22 对账、Trap 23 排渣隔离后的现状:新增 `net_recv_to` / `net_sock_state` / `net_resync_listen` / `net_relisten`(§10.1)及 `net_getsockname`;§3 新增 3.6(对账与自愈四机制)、3.3/3.5 改写(recv 挂起模型、队列语义、netTask 运行要求);§4/§5 示例更新(监听由就绪屏障后服务任务建立、`net_sock_state` 轮询);§10.2 改述 `net_process_messages()` 每轮节奏(重监听重试 → 挂起 recv 完成 → 200ms 对账 → 队列),补 `timeout_ms`/新消息类型说明;新增 §11.2 分片阻塞读(`net_recv_to`);§12.1/12.3/12.4/12.9 更新,新增 §12.10(自愈行为数据与排障判据,含 Trap 21 结案) | | 2026-09-21 | **修正公开 API 的错误码回传**:`net_msg_t` 按值拷入队列 → netTask 对消息的写入调用方不可见,改用 `xTaskNotify()` 通知值回传具体错误码(负值),公开 API 恢复 errno 后统一返回 -1(此前被覆盖成 `NET_ERR`,使 `net_get_errno() != NET_ERR_WOULDBLOCK` 的"空轮询"判断恒成立 → `ui_net_sink` 连上即 `net_close`);§12.4 补单连接模型断开处理(EOF 时不得 `net_close` 监听 socket);§12.9 补错误码回传约束 | | 2026-09-15 | 同步配置引擎属主变更(P1-3):`net_init` 调用方说明改为"netTask 只读 RAM 副本"(网口配置由 defaultTask 在就绪屏障前加载);模拟器端启动序列补 `net_cfg_save_run()`(主循环 = 模拟器侧配置引擎属主,执行 UI 保存请求) | diff --git a/docs/网络/BSD_Socket_API_函数调用关系.md b/docs/网络/BSD_Socket_API_函数调用关系.md index 938da2a5..2100e955 100644 --- a/docs/网络/BSD_Socket_API_函数调用关系.md +++ b/docs/网络/BSD_Socket_API_函数调用关系.md @@ -4,7 +4,7 @@ > 使用指南回答"**上层该怎么调**"(公开语义 / 示例 / 常见问题);本文回答"**内部函数各自做什么、互相怎么调用、为什么这么设计**"(调用关系 / 设计意图 / 踩坑)。 > > **范围**:MCU 端 `Drivers/BSP/NET/net_socket.c`(约 2870 行,全部函数)。 -> 模拟器端后端 `simulator/sim/net_socket_sim.c`(Winsock)是另一套实现,见使用指南 §2.3;`net_select.c` 不在此展开,见使用指南 §7 / §10.5。 +> 模拟器端后端 `simulator/sim/net_socket_sim.c`(Winsock)是另一套实现,见 `docs/网络/BSD_Socket_API_使用指南.md` §2.3;`net_select.c` 不在此展开,见 `docs/网络/BSD_Socket_API_使用指南.md` §7 / §10.5。 > > **交叉引用约定**:机制标 Trap 号 → `docs/问题记录/CH395F_Trap_Records.md`;用法语义引用使用指南 §;关键参数值与 `net_config.h` / `net_socket.c` 内宏一致。 @@ -103,7 +103,7 @@ flowchart TB | 函数 | 设计作用 | 主要调用 | 意图 / 注意 | |---|---|---|---| -| `net_init(ip,mask,gw,mac)` | 芯片一次性配置:探测/复位 → 参数 → MAC → 协议栈初始化 → 8 Socket 缓冲布局 → 使能 PING | `net_pending_cancel_all` → `ch395f_check_exist` / `ch395f_reset` / `set_fun_para(0x09)` / `set_tcp_mss` / `set_retrans_count+period` / `set_keepalive_idle+intvl+cnt` → `set_mac_addr` → `parse_ip_string` + `set_ip_addr` / `set_gwip_addr` / `set_mask_addr`(或 `set_dhcp`)→ `ch395f_init` → `set_send_buf` / `set_recv_buf` ×8 → `ping_enable` | ① **重建前**先取消全部挂起 recv 并逐一通知(不通知 = 调用方永久挂死),且必须早于控制块清零与芯片复位;② 重传/KeepAlive/**MAC**/IP/GW/MASK 参数**必须**在 `ch395f_init` 之前下发(手册 §9.2.1:SET_MAC 为步骤①,INIT_CH395 加载 MAC——顺序反了本次上电不生效,P1-7);③ `fun_para=0x09`:多 Socket 模式 + TCP Server 单连接 + DISCONNECT/TIMEOUT 不清缓冲(软件排空后手动关);④ 缓冲布局每 Socket 6 块(收 4 + 发 2)×8 = 48 块恰好用尽,Socket 0~7 必须显式重分配(Trap 01/02);⑤ 捕获 `xTaskGetCurrentTaskHandle` 存入 `s_net_task_handle`(防自通知死锁的判据) | +| `net_init(ip,mask,gw,mac)` | 芯片一次性配置:探测/复位 → 参数 → MAC → 协议栈初始化 → 8 Socket 缓冲布局 → 使能 PING | `net_pending_cancel_all` → `ch395f_check_exist` / `ch395f_reset` / `set_fun_para(0x08)` / `set_tcp_mss` / `set_retrans_count+period` / `set_keepalive_idle+intvl+cnt` → `set_mac_addr` → `parse_ip_string` + `set_ip_addr` / `set_gwip_addr` / `set_mask_addr`(或 `set_dhcp`)→ `ch395f_init` → `set_send_buf` / `set_recv_buf` ×8 → `ping_enable` | ① **重建前**先取消全部挂起 recv 并逐一通知(不通知 = 调用方永久挂死),且必须早于控制块清零与芯片复位;② 重传/KeepAlive/**MAC**/IP/GW/MASK 参数**必须**在 `ch395f_init` 之前下发(`docs/芯片手册/CH395F.md` §9.2.1:SET_MAC 为步骤①,INIT_CH395 加载 MAC——顺序反了本次上电不生效,P1-7);③ `fun_para=0x08`:bit3 SOCKET_CLOSE(DISCONNECT/TIMEOUT 后不清缓冲,软件排空后手动关);bit0/bit1 保持 0——8 个 Socket 由索引 `_SN` 寻址 + 缓冲显式分配实现,与 FUN_PARA 无关(bit0 是保留位,必须为 0);④ 缓冲布局每 Socket 6 块(收 4 + 发 2)×8 = 48 块恰好用尽,Socket 0~7 必须显式重分配(Trap 01/02);⑤ 捕获 `xTaskGetCurrentTaskHandle` 存入 `s_net_task_handle`(防自通知死锁的判据) | | `net_get_generation()` | 读网络代次号(纯 RAM) | 无 | FTP/UI/对时服务据此感知"网络已重建"→ 放弃旧监听并重建 | | `net_generation_inc()` | 代次号 +1 | 无 | 唯一合法递增点 = `net_stack_bring_up()` 末尾(PHY/链路就绪后)——太早递增会让服务在网络不可用时抢跑重建;模拟器端恒为 0(无重建概念,上层自愈逻辑空转) | @@ -318,7 +318,7 @@ sequenceDiagram N->>A: net_init 开头:net_pending_cancel_all(CONNRESET)——逐一取消并通知 N->>N: 控制块全部清零 - N->>C: check_exist → reset → fun_para(0x09)/MSS/重传/KeepAlive + N->>C: check_exist → reset → fun_para(0x08)/MSS/重传/KeepAlive N->>C: IP·GW·掩码(或 DHCP)→ ch395f_init → MAC → 8 Socket 缓冲布局 Note over N,A: net_stack_bring_up:PHY/链路就绪 → net_generation_inc
FTP/UI/对时据**代次变化**放弃旧监听并重建(见 docs/子系统设计/网络配置说明.md §4) ``` @@ -347,9 +347,9 @@ sequenceDiagram | 芯片命令 | 使用处 | 说明 | |---|---|---| | `ch395f_check_exist` / `ch395f_reset` | `net_init` | 上电探测与复位 | -| `ch395f_set_fun_para(0x09)` | `net_init` | 多 Socket + TCP 单连接 + 手动关缓冲 | +| `ch395f_set_fun_para(0x08)` | `net_init` | bit3 手动关缓冲(SOCKET_CLOSE);bit0/1=0,Socket 个数与 FUN_PARA 无关 | | `ch395f_set_tcp_mss` / `set_retrans_count` / `set_retrans_period` / `set_keepalive_idle+intvl+cnt` | `net_init`(**均须在 `ch395f_init` 之前**) | 值见 `net_config.h` | -| `ch395f_set_mac_addr` | `net_init`(**须在 `ch395f_init` 之前**) | 写芯片 EEPROM;INIT_CH395 加载 MAC(手册 §9.2.1 步骤①,P1-7) | +| `ch395f_set_mac_addr` | `net_init`(**须在 `ch395f_init` 之前**) | 写芯片 EEPROM;INIT_CH395 加载 MAC(`docs/芯片手册/CH395F.md` §9.2.1 步骤①,P1-7) | | `ch395f_set_ip_addr` / `set_gwip_addr` / `set_mask_addr` / `set_dhcp` | `net_init`(**均须在 `ch395f_init` 之前**) | 固定 IP 或 DHCP | | `ch395f_init` / `ch395f_ping_enable` | `net_init` | 协议栈初始化(INIT_CH395)/ PING(须在 init 之后) | | `ch395f_set_send_buf` / `ch395f_set_recv_buf` | `net_init`(8 Socket 布局);`net_bind`(UDP)(Socket 4~7 兜底) | 每 Socket 收 4 块 + 发 2 块 | diff --git a/docs/网络/Net层阻塞模型架构设计.md b/docs/网络/Net层阻塞模型架构设计.md index ed95d301..e181ed8a 100644 --- a/docs/网络/Net层阻塞模型架构设计.md +++ b/docs/网络/Net层阻塞模型架构设计.md @@ -9,7 +9,7 @@ > `docs/网络/BSD_Socket_API_使用指南.md`(应用视角 API 语义)、 > `docs/网络/BSD_Socket_API_函数调用关系.md`(调用链与执行域)、 > `docs/测试说明/链路延迟测试说明.md`(延迟基线与故障对照)、 -> `docs/待处理内容.md` §5.1(二期 ACCEPT/SEND 扩展设计) +> `docs/任务规划/基线与待确认.md` §2.1(二期 ACCEPT/SEND 扩展设计) --- @@ -147,7 +147,7 @@ CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的 | **模拟器不可建模** | `net_socket_sim.c` 单线程 Winsock 无法复现挂起模型 | 该路径在模拟器上零覆盖,只能硬件验证 | **二期扩展设计**(ACCEPT / SEND 的挂起化、门禁清单、改动量)见 -`docs/待处理内容.md` §5.1。 +`docs/任务规划/基线与待确认.md` §2.1。 --- @@ -208,7 +208,7 @@ CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的 - 看门狗接 selftest 留痕 → 二期; - 模拟器差距 +1(`net_socket_sim.c` 无法建模挂起)→ 二期更新清单; - 台架期间另发现一次 49s 全系统日志静默(defaultTask/NAND syslog 排空停滞,net 栈照常工作) - ——既有存储侧现象,与本层无关,见 `docs/待处理内容.md` N-2。 + ——既有存储侧现象,与本层无关,见 `docs/任务规划/README.md` N-2。 ## 附录 B:修订记录 diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index c58ac459..476d0853 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -19,7 +19,7 @@ ### 注意事项 - 数据 Socket 不调 `OPEN_SOCKET`,由 CH395F 在连接到达时自动打开 - DISCONNECT 后 CH395F 自动关闭 Socket,但保留协议/端口/缓冲区配置,可被再次分配 -- 手册 §9.2.6 要求数据 Socket **先设源端口再设协议类型**(与监听 Socket 顺序相反) +- `docs/芯片手册/CH395F.md` §9.2.6 要求数据 Socket **先设源端口再设协议类型**(与监听 Socket 顺序相反) ### 对应文件 `Drivers/BSP/NET/net_socket.c` → `net_listen()` @@ -35,7 +35,7 @@ Phase 8 压力测试,2025-07 多连接模式下多个 Socket 数据互相覆盖,收到错误数据或客户端挂死。 ### 根因 -CH395F 默认只为 Socket 0~3 各分配独立收发缓冲(共 48 块 × 512B 用满,见手册 §5.45),Socket 4~7 为零块。`net_listen()` 未对 Socket 4~7 显式分配独立缓冲区块,后续代码误用相同块号(如 28~31)导致数据重叠。 +CH395F 默认只为 Socket 0~3 各分配独立收发缓冲(共 48 块 × 512B 用满,见`docs/芯片手册/CH395F.md` §5.45),Socket 4~7 为零块。`net_listen()` 未对 Socket 4~7 显式分配独立缓冲区块,后续代码误用相同块号(如 28~31)导致数据重叠。 ### 解决方案 每个数据 Socket 独占 4 个缓冲区块(2 发 + 2 收),按 `ds * 4` 基址分配,确保不重叠: @@ -154,11 +154,35 @@ Phase 2 测试,2025-07 `disconnect` 发送 FIN 将 Socket 推入 `FIN_WAIT_2`,此后 `close` 不再发 RST,必须等远端发 FIN 才能关闭。 ### 解决方案 -直接调用 `ch395f_close_socket()`,在 ESTABLISHED 或 CLOSE_WAIT 下会发 RST 立即终止。 -关闭后轮询 `ch395f_get_socket_status()` 等待 `sock=0x00` 再 `open_socket`。 +直接调用 `ch395f_close_socket()`(**做法保留**,原"发 RST"的说法见下方定案更正)。 +关闭后轮询 `ch395f_get_socket_status()` 的**第 1 字节**等待 `sock=0x00` 再 `open_socket`。 + +### 定案更正(2026-10-11):机制说法作废,实测 `disconnect → close` 不卡死 + +新增定案用例 **TC-NET-204**(`test/board/ch395f_test_task.c`,用例表见 `docs/测试说明/CH395F_Test_Guide.md`):同一 Socket、同一 PC 回显服务、`FUN_PARA=0x08`、硬件层独占,三模式各 5 轮(45 项判定全过): + +| 模式 | 关闭序列 | 回到 `SOCKET_CLOSED` 耗时(5 轮) | 重开+重连+回显 | +|---|---|---|---| +| A | `close` | 0 / 0 / 150 / 0 / 0 ms | 5/5 OK | +| B | `disconnect → close` | **0 / 0 / 0 / 0 / 0 ms** | 5/5 OK | +| C | `disconnect`(不 close) | **10 s 内从未回到**,byte0 恒 `0x05`、byte1 `0x00` | 直接 `open` → **`0x20` FAIL**(5/5);补 `close` 后 +0 ms 回 CLOSED,重开 5/5 OK | + +**三条推翻原根因**: + +1. **`FIN_WAIT_2`(0x06) 全程未出现**(15 轮 × 9 个采样点)。实测过渡态只有 `TIME_WAIT`(0x0A)(A 模式一轮在 +0/+50/+100 ms 可见,+150 ms 回 CLOSED)与 `TCP_CLOSED`(0x00)。 +2. **`close` 无 RST 语义**:§5.35 只说"关闭 Socket 前 CH395 会自动断开 TCP 连接"。手册中芯片主动发 RST 只出现在 §5.29(监听 Socket 已有连接时再来连接 →"发送 TCP RESET")与 §9.2.6(多连接找不到可用数据 Socket →"将连接复位")两处,均与 `close`/`disconnect` 无关;此外仅有错误码 `CH395_ERR_RST` 与 RST/RSTI 引脚。且主动发 FIN 先进 `FIN_WAIT_1`(05H)、收到 ACK 才进 `FIN_WAIT_2`(06H)——"disconnect 推入 FIN_WAIT_2"本身就跳了一拍。 +3. **观测口径错在把 byte1 当独立事实**:§5.22 第 1 字节只有 `00H/05H` 两个取值,第 2 字节才是 TCP 态;官方 SDK `CH395CMDGetSocketStatus()` 注释明确"Second byte — TCP state, **meaningful only if** TCP mode **and the first byte is open**"。当年 Phase 2 收尾代码的判据用的是**正确的 byte0**(`st[0]==CH395F_SOCKET_CLOSED`),break 之后照样打印 byte1 → 一个"已 CLOSED 状态下的残留 TCP 态读数"被读成了"卡在某个 TCP 态"。 + +**真正的原因**(C 模式 5/5 复现):`FUN_PARA` bit3 `SOCKET_CLOSE=1`(§5.52 ②,`0x08`/`0x09` 都置了)下,产生 DISCONNECT 后**芯片不关 Socket、不清缓冲**,Socket 长期停在 `byte0=0x05(OPEN) / byte1=0x00(TCP_CLOSED)`;此时直接 `open_socket` 必返 **`0x20` FAIL**——正是 2026-07-19 测试指南问题表里那条 `Phase 2 open_socket: 0x20 (FAIL) / socket 未完全关闭`。**把 Socket 拉回 CLOSED 的动作是 `close`,与 RST 无关。** + +**git 侧佐证**:`ch395f_tcp_disconnect()` 自首个提交起**只有定义、零调用点**(`git log --all -S` 在 `NET/App/test` 仅命中本仓 `bca6cc6` 一次),"disconnect→close" 序列从未进入任何提交代码;本条"发现时间 2025-07"也与仓库不符(仓库首个提交 `b1d05091` = 2026-06-05,Phase 2 对应 2026-07-19 的 `17109d9b`→`7a8a97f4`→`a08d26bc`)。当时驱动已在轮询 `GET_CMD_STATUS`(4 s 上限),故也不是"命令没等完成"。 + +**做法为何仍然对**(换成三条有支撑的理由):① §5.35 `close` 已自动断链,`disconnect` 纯属多余;② `disconnect` 会**制造**一个 DISCONNECT 事件,bit3=1 下芯片不自动收尾,该事件必须被消费/预清,否则污染下一条连接(Trap 14 / 17 / 22 实证);③ 它把芯片推进 closing 过渡态,重开需要沉降(Trap 09)。本轮 15 轮重开均未观察到残留 `DISCONNECT`(0x10)——`close` 顺带清掉了锁存,这正是"直接 close"安全的原因。 + +**与 Trap 17 的张力已消解**:Trap 17 写"close 后芯片要等对端 FIN/ACK 才把 Socket 置 CLOSED(耗时数毫秒)",与本条原"close 发 RST 瞬间回 CLOSED"矛盾;实测支持 Trap 17 一侧(A 模式可见 `TIME_WAIT` 过渡、0~150 ms),不支持 RST 说法。 ### 发现时间 -Phase 2 TCP Client 测试,2025-07 +Phase 2 TCP Client 测试,2025-07(**日期有误**,实为 2026-07-19;定案更正与 TC-NET-204 实测 2026-10-11) --- @@ -190,7 +214,7 @@ CH395F 的 RECV 中断是电平触发的——只要接收缓冲区有数据就 ### SPI 通信 - 每次 SPI 事务需调用 `ch395f_spi_begin()` / `ch395f_spi_end()` 包裹 - **大数据量收发使用 SPI2 DMA**:`ch395f_write_send_buf()` 和 `ch395f_read_recv_buf()` 已改造为 DMA 批量传输(DMA1_Stream3 RX, DMA1_Stream4 TX),命令和配置操作仍使用逐字节轮询 -- DMA 缓冲区:`s_spi2_dma_tx_buf[1500]` / `s_spi2_dma_rx_buf[1500]`,4 字节对齐 +- DMA 缓冲区:`s_spi2_dma_tx_buf/rx_buf[CH395F_SPI_DMA_BUF_SIZE]` = 接收缓冲 2048 + 4 = **2052**(`ch395f.h:148`),4 字节对齐;单次有效载荷上限 2048(历史值 1500 / 4100 均已作废,见 Trap 13 复核注) ### TCP 参数配置(必须在 INIT_CH395 之前) - **重传参数**:`SET_RETRAN_COUNT`(默认12次,最大20)和 `SET_RETRAN_PERIOD`(默认500ms,最大1000ms),总重传时间 = 次数 × 周期 @@ -205,10 +229,11 @@ CH395F 的 RECV 中断是电平触发的——只要接收缓冲区有数据就 - 不分配会导致发送数据为固定垃圾内容(`0x0028` 填充)、接收缓冲区无法存储数据 ### TCP 关闭重连 -- **直接调用 `ch395f_close_socket()`,不要先调 `ch395f_tcp_disconnect()`** -- ❌ `disconnect → close`:disconnect 发 FIN 推入 FIN_WAIT_2,close 不再发 RST,Socket 卡住数分钟 -- ✅ `close` 直接:在 ESTABLISHED 或 CLOSE_WAIT 下 close 发 RST 立即终止,瞬间回到 CLOSED -- 关闭后需轮询 `ch395f_get_socket_status()` 等待 `sock=0x00` 再 `open_socket` +- **直接调用 `ch395f_close_socket()`,不要先调 `ch395f_tcp_disconnect()`**(做法不变;机理口径已于 2026-10-11 由 TC-NET-204 实测更正,见 Trap 07「定案更正」) +- ✅ `close` 直接:§5.35 关闭前芯片**自动断链**,无需先 disconnect;实测 0~150 ms 回到 `SOCKET_CLOSED`,过渡态可见 `TIME_WAIT`(0x0A)——**不是 RST**(手册无 RST 语义) +- ❌ 只 `disconnect` 不 `close`:`FUN_PARA` bit3=1(§5.52 ②)下芯片**不自动关 Socket**,Socket 长期停在 `0x05(OPEN)/0x00(TCP_CLOSED)`,再 `open_socket` 必返 **`0x20` FAIL** +- ❌ `disconnect → close` 本身**不卡死**(实测 5/5 全部 +0 ms 回 CLOSED),只是多余:它制造一个 DISCONNECT 事件,而 bit3=1 下该事件须由我方消费/预清(Trap 14 / 17 / 22) +- 关闭后需轮询 `ch395f_get_socket_status()` 的**第 1 字节**等待 `sock=0x00` 再 `open_socket`;**第 2 字节(TCP 态)仅在 byte0=OPEN 时有意义**,别拿它当"卡住"的判据 ### SOCK_TIMEOUT 处理 - 长时间无数据时可能触发 `SINT_STAT_SOCK_TIMEOUT`,**不应视为致命错误**——记录日志后继续操作即可,不要因此关闭 Socket @@ -386,6 +411,8 @@ MobaXterm 上传文件时 CH395F 收到数据(`s1 int=0x04` RECV_OK 持续触 ### 根因 `ch395f_read_recv_buf` 读部分数据后,CH395F 的 `recv_len` 寄存器**被重置为 0**(非递减)。缓冲区中剩余的 1896 字节仍然存在(RECV_OK 标志为真),但 `ch395f_get_recv_len` 报告 0。 +> ⚠ **2026-10-11 复核:该机理未证实,且与手册直接相左——§5.34 明文"读取的数据长度可以小于缓冲区的实际有效数据长度,未读取的数据仍然保留在接收缓冲区中,单片机可以通过此命令继续读取";本仓自己的 TC-NET-201 注释也是相反口径(`test/board/ch395f_test_task.c:419` "只查询积压字节数…可反复调用"、`:476-477` "剩余字节下一轮 get_recv_len 会继续报告,不会丢")。同族结论"部分排空致命"已由 Trap 21 结案撤回,本条当时未同步。现象本身(当年确实观察到 `recv_len` 归零)保留为历史记录;定案前须补一条"部分读 → 再查 `recv_len`"的定向 TC。下方解法数字均为历史值:现 `CH395F_SPI_DMA_BUF_SIZE = 2048+4 = 2052`(`ch395f.h:148`)、`lftpd.c:228` 是 `static unsigned char buffer[1024]`,`s_recv_buf[4096]` 已不存在。** + 同时 `CH395F_DMA_BUF_SIZE=1500`(header 4 + max 1496 data),无法一次读取完整的 4096 字节 CH395F 接收缓冲区。 ### 解决方案 @@ -441,7 +468,7 @@ DHCP 状态位仅代表协商流程完成;IP/MASK/GW 寄存器的提交与状 NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报 `sock0 CONNECT TIMEOUT`,连接永远建立不起来;而硬件层 TCP Client(阶段 2,同一 `net_init`/FUN_PARA=0x08,同一目标 192.168.1.2:8081)完全正常。PC 同网段、防火墙关闭、服务端正常监听。 ### 根因 -`net_connect_locked()` 把 `sockaddr_in.sin_addr.s_addr`(网络字节序,`net_inet_addr` 返回)按 **低字节在前** 拆成 `remote_ip_arr[4]`,得到 `[2,1,168,192]`,再交给 `ch395f_set_des_ip()`。但 **CH395F 的目的 IP/端口寄存器是大端(网络字节序)**(阶段 2 用 `test_parse_ip("192.168.1.2")` 直接得到 `[192,168,1,2]` 即成功,可证)。于是芯片把 SYN 发往 `2.1.168.192`——一个跨网段、不可达的地址,SYN 被网关丢弃 → 无 SYN-ACK → `CONNECT TIMEOUT`。 +`net_connect_locked()` 把 `sockaddr_in.sin_addr.s_addr`(网络字节序,`net_inet_addr` 返回)按 **低字节在前** 拆成 `remote_ip_arr[4]`,得到 `[2,1,168,192]`,再交给 `ch395f_set_des_ip()`。但 **`SET_IP_ADDR_SN` 族按点分顺序(`[192,168,1,2]`)写入才生效**(阶段 2 用 `test_parse_ip("192.168.1.2")` 直接得到 `[192,168,1,2]` 即成功,可证;官方例程 `EXAM5_TCP_Server/main.c:39 CH395IPAddr={192,168,1,101}` 同向)。⚠ 2026-10-11 复核:手册 §5.8 的总括"所有包含 IP 输入或输出的命令,字节顺序均为 IP 低字节在前"与官方例程自相矛盾,本仓以实测 + 例程为准;**端口不在此列**——§5.25/§5.26 明确低字节在前,驱动 `ch395f_set_des_port`/`set_sour_port` 即如此且工作正常,勿把"大端数组"套到端口上。于是芯片把 SYN 发往 `2.1.168.192`——一个跨网段、不可达的地址,SYN 被网关丢弃 → 无 SYN-ACK → `CONNECT TIMEOUT`。 **误导性排查**:`CMD_GET_REMOT_IPP_SN` 回读的是**小端**字节序(`ch395f.c` 注释 "IP和端口均为低字节在前")。把写反的 `[2,1,168,192]` 读回再按小端解释,恰好又得到 `192.168.1.2`,使回读日志看起来"目的地址正确",掩盖了真实写反。 @@ -452,7 +479,7 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报 ### 注意事项 - `net_inet_addr` 返回网络字节序;`sin_addr.s_addr` 一律按网络字节序理解。 -- 任何写 CH395F IP/端口寄存器(set_des_ip / set_ip_addr / set_gwip_addr / set_mask_addr)都传**大端数组**,与 `test_parse_ip` 产物一致。 +- 任何写 CH395F **IP 四字节数组**(set_des_ip / set_ip_addr / set_gwip_addr / set_mask_addr)都传**点分顺序数组**,与 `test_parse_ip` 产物一致;**16 位端口一律低字节在前**(§5.25/§5.26),两者口径不同。 - 回读类命令(GET_REMOT_IPP / GET_IP_INF 等)若文档标注"低字节在前",打印时务必先按小端重组再格式化,否则会误判。 ### 对应文件 @@ -489,27 +516,51 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报 ## Trap 18:多 Socket 模式切换 + 8 个 Socket 缓冲分配 ### 现象 -阶段 7 仅测了 Socket 0(单连接模式只有 Socket 0 可用)。要验证 Socket 1~7 也能作为 client,必须把 `FUN_PARA` 从单 Socket 模式切到多 Socket 模式,否则 `open_socket(1~7)` 被芯片拒绝/无响应,1~7 完全不可用。 +阶段 7 仅测了 Socket 0。要验证 Socket 1~7 也能作为 client,当时**推论**必须把 `FUN_PARA` 从"单 Socket 模式"切到"多 Socket 模式",否则 `open_socket(1~7)` 被芯片拒绝/无响应,1~7 完全不可用。 + +⚠ **2026-10-11 复核:这句"必须切多 Socket 模式"是当时的前置推论,不是实测记录**——本段无串口日志摘录、无 TC 编号、无主机侧独立证据;且当年 Socket 1~7 **根本没有显式分配缓冲**(`net_init` 注释"先不配置 buffer"、MSS=1460),按 Trap 01/02 未配缓冲的 Socket 中断不触发,混淆因子未被排除。bit0 语义见本节末「定案更正」。 ### 根因 `CMD_SET_FUN_PARA` 的 **bit0** 决定 Socket 数量模式:`0`=单 Socket 模式(仅 Socket 0,8KB 缓冲全归它);`1`=多 Socket 模式(Socket 0~7 全部可用,缓冲需自行分配)。之前 `net_init` 用 `0x08`(bit0=0)即单 Socket 模式,故 1~7 不存在。注意 bit1(TCP Server 单/多连接)与 bit0 是两回事:本方案只翻 bit0(多 Socket),bit1 保持 0(TCP Server 单连接),即"8 个 Socket 可用,但 Socket 0 作服务端时只接 1 客户端"。 +⚠ **此"根因"已于 2026-10-11 证伪**(bit0 在所有版本官方手册里都是「保留,必须为0」;官方例程 bit0=0 下 8 个 Socket 照常可用)。当年真正起作用的是下面第 2 条的**缓冲显式分配**,不是 bit0。完整证据见本节末「定案更正」。 + ### 解决 -1. `net_init`:`ch395f_set_fun_para(0x09)`(bit0=1, bit1=0, bit3=1)。 -2. `ch395f_init()` 之后,循环 0~7 用 `ch395f_set_send_buf(sock, i*6+4, 2)` / `ch395f_set_recv_buf(sock, i*6, 4)` 给每个 Socket 分配独立收发缓冲。按手册 §5.45(48 块 × 512B = 24KB)+ §9.2.8 最优分配:每 Socket 6 块 = 接收 4 块(2KB) + 发送 2 块(1KB),`ch395f_set_tcp_mss(1024)`,恰好用尽 0~47 块、互不重叠。 +1. `net_init`:`ch395f_set_fun_para(0x09)`(bit0=1, bit1=0, bit3=1)。⚠ **2026-10-11 已改回 `0x08`**(bit0 保留必须为 0,见「定案更正」)。 +2. `ch395f_init()` 之后,循环 0~7 用 `ch395f_set_send_buf(sock, i*6+4, 2)` / `ch395f_set_recv_buf(sock, i*6, 4)` 给每个 Socket 分配独立收发缓冲。按`docs/芯片手册/CH395F.md` §5.45(48 块 × 512B = 24KB)+ §9.2.8 最优分配:每 Socket 6 块 = 接收 4 块(2KB) + 发送 2 块(1KB),`ch395f_set_tcp_mss(1024)`,恰好用尽 0~47 块、互不重叠。 3. MSS 选取依据 §9.2.8:接收缓冲(2KB) ≥ 2×MSS(1024) 满足建议、≥ MSS 满足必须;发送缓冲(1KB) ≤ 8KB 满足必须。MSS 取 1024 为 8-Socket 共享 24KB 时的最大值。 -4. `net_init` 缓冲分配顺序遵循 Trap 01:先配数据 Socket,再配监听 Socket 0(本 Net 层 Socket 按需 open,故统一在 init 一次性配好)。UDP 路径 `net_socket()` 中对 Socket 4~7 的兜底重设也同步为同一布局。 -5. PC 端 `tcp_server` 改为每连接一线程的并发回显(`--max-conn` 为并发计数),否则 Phase 7 全量连接会触发累计计数上限而拒掉 4~7(Trap 19)。 +4. `net_init` 缓冲分配顺序遵循 Trap 01:先配数据 Socket,再配监听 Socket 0(本 Net 层 Socket 按需 open,故统一在 init 一次性配好)。UDP 路径 `net_bind()`(`net_socket.c:1000-1014`)中对 Socket 4~7 的兜底重设也同步为同一布局。 +5. PC 端 `tcp_server` 改为每连接一线程的并发回显(`--max-conn` 为并发计数),否则 Phase 7 全量连接会触发累计计数上限而拒掉 4~7。 ### 注意事项 - 4~7 的缓冲必须显式分配,否则其 CONNECT/RECV 中断不触发(Trap 01/02)。 -- 上述布局**以手册 §5.45 的 48×512B 缓冲几何为准**,并已由 Phase 7 TC704(40/40 PASS)实测确认块 0~47 全部有效(此前文档中"32×1KB"记录有误)。Socket 4~7 分配与 8 路并发回显均验证通过。 +- 上述布局**以`docs/芯片手册/CH395F.md` §5.45 的 48×512B 缓冲几何为准**,并已由 Phase 7 TC704(40/40 PASS)实测确认块 0~47 全部有效(此前文档中"32×1KB"记录有误)。Socket 4~7 分配与 8 路并发回显均验证通过。 - 多 Socket 模式下 TCP Server 单连接语义不变:Socket 0 监听且连接停留在本 Socket,Sockets 1~7 由应用当作独立 client 使用。 - `net_poll` 用 `GET_GLOB_INT_STATUS_ALL`(2 字节,支持 0~7)并遍历 0~7,Socket n 中断位即 `bit(n+4)`(Socket 0~3→bit4~7,Socket 4~7→bit8~11),原代码正确无需改动。 ### 对应文件 `Drivers/BSP/NET/net_socket.c`(`net_init` FUN_PARA + 缓冲分配 + MSS);`test/board/net_test_task.c`(新增 TC704 八 Socket 并发回显);`test/host/ch395f_socket_test.py`(`tcp_server` 并发 + `--max-conn`)。 +### 定案更正(2026-10-11):`FUN_PARA` 取 **0x08**,bit0 与 Socket 个数无关 + +**结论**:`net_init` 用 `ch395f_set_fun_para(0x08)`(bit3 SOCKET_CLOSE=1、bit0=0)。本节"bit0=单/多 Socket 模式"的说法**无任何出处,已撤回**;当年让 Socket 1~7 真正可用的是上面第 2 条的**缓冲显式分配**,不是 bit0。 + +**证据(一手,按强度排序)**: + +1. **官方手册位表**:本机 `D:\Code\DTU\使用的芯片手册\以太网控制器_CH395F(1).pdf`(WCH《CH395 手册 1》V2.2,页眉 `https://wch.cn`)§5.52 表跨 PDF 第 21→22 页,bit0 行原文 `0 - - 保留,必须为0`;bit2、`[5:6]`、`[31:8]` 同为保留;注释①~⑥ 无一处提 Socket 数量。官方现行版 V2.3 逐位一致。旧版(V1C、英文 V1E、2020 版 CH395L/Q)bit0 **同样是保留**(旧版多出的位是 bit2=LOW_PWR,V2.2 起收回为保留)→ **任何版本都不存在"bit0=多 Socket"这一行**。 +2. **官方 SDK 位宏**:`CH395EVT\EVT\EXAM\PUB\CH395INC.H:349-353` 只定义 bit1/3/4/7/8,**没有 bit0 宏**。 +3. **官方例程直接反例**:`EXAM5_TCP_Server\main.c:296` 传 `0x02`(bit0=0),同文件 `:337-359` 给 Socket 0~7 全部配缓冲 → **bit0=0 下 8 个 Socket 照常可用**;`EXAM11_FTPClient\FTP\CH395.C:319` 官方直接传 `0x08, 0x00, 0x00, 0x00`(与本仓取值逐字节相同);全 EVT 包 grep `SetFunPapr` 仅 3 处(0x02 / 0x02 / 0x08),**无一处 0x09**。 +4. **8 Socket 是芯片原生能力**:`docs/芯片手册/CH395F.md` 命令章前言「CH395 内部可以同时提供 8 个 Socket,其索引值分别为 0、1、2、3、4、5、6、7」;Socket 类命令一律 `_SN`(Socket 索引)寻址。 +5. **真正的门槛是缓冲**:`docs/芯片手册/CH395F.md` §5.45「如果单片机需要用到的 Socket 个数大于 4,需要重新对缓冲区进行分配」——正是本节第 2 条措施。 +6. **概念讹传源**:手册里唯一的"多"字样是 bit1 `FUN_PARA_FLAG_TCP_SERVER`(TCP 服务器**多连接**,全局配置),`docs/芯片手册/CH395F.md` §9.2.6「默认的设置下,服务器多连接功能被关闭……每个 Socket 只可以建立一个 TCP 连接」——单/多之分指**一个服务器能接几个客户端**,不是"能用几个 Socket"。 +7. **互联网侧**:多轮检索无任何来源称 bit0 为单/多 Socket 模式,无公开代码传 0x09;CH390/CH392/CH397 同族也无此位可串源。 + +**为什么 0x09 当年"看起来有效"**:芯片忽略了保留位(Trap 25 旁证:bit0=1 下 sock1/2/3 并行正常)。TC704 40/40 PASS 只证明"0x09 + 缓冲布局"通过,**不构成"0x08 下不通过"的对照**(与 bit0 结论出自同一提交,属自证)。留着 bit0=1 的代价是违反保留位规定——后续固件版本一旦启用该位,现网设备静默失效。 + +**实测闭环(2026-10-11 20:18,N-31 已销项)**:`FUN_PARA=0x08` 下跑 TC-NET-704(`PHASE7_MULTI_COUNT` 降到 5:E-23 未修,`NET_MAX_SOCKETS=8` 而 FTP/对时/远显三个监听常驻占 3 个 fd)→ **Phase 7 34/34 全通过(failed=0)**:5 路并发全部 ESTABLISHED、每路 64 B 回显字节级比对 OK;芯片 Socket 实测落在 **sock2 / sock4 / sock5 / sock6 / sock7**(0/1/3 被三个监听占用)→ **Socket 4~7 在 bit0=0 下完全可用**,与手册口径一致,定案成立。烧录后回读 `No difference found`。 + +⚠ **台架前置(本轮踩到,务必先排)**:① PC 的以太网适配器落在 **Public** 防火墙配置文件 → 入站 8081 被静默丢弃,症状是 TC701/702/704 **全部** `CONNECT TIMEOUT`(而 PC→设备 21 端口正常、ping 通、ARP 正常),**极易误判成"0x08 毁了 client"**;② 上一轮遗留的 `tcp_server` 进程仍占着 8081,新起的服务器绑不上端口却不报错。两条都排除(临时关 Public 配置文件 + 清残留 python 进程)后才拿到上面的结果——**判"芯片行为"前先确认对端真的收到了 SYN**(Trap 25 总纲铁律的又一次命中)。 + ### 发现时间 阶段 7 扩展 8 Socket 测试,2026-08-25。 @@ -603,7 +654,7 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报 自愈实测计数(整轮会话):`chip closed -> disconnect` **12** 次、`connection completed w/o CONNECT irq -> accept` **1** 次、`listen lost -> relisten` **1** 次——三类失同步均真实出现并被自愈;`relisten give up` 0 次。 ### 对应文件 -`Drivers/BSP/NET/net_socket.c`(`net_reconcile_tick` / `handle_disconnect_event` LISTENING 分支);`MDK-ARM/tcp_reconnect_probe.py`(可信用例);`docs/待处理内容.md` N-6(✅ 已销项,2026-09-26 清理)。 +`Drivers/BSP/NET/net_socket.c`(`net_reconcile_tick` / `handle_disconnect_event` LISTENING 分支);`MDK-ARM/tcp_reconnect_probe.py`(可信用例);`docs/任务规划/README.md` N-6(✅ 已销项,2026-09-26 清理)。 ### 发现时间 2026-09-22(台架偶发 1/1)→ 2026-09-23(生产固件确定性复现 + 当日修复闭环)。 @@ -738,13 +789,13 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报 --- -## Trap 24:MAC 写入顺序违反手册 §9.2.1(SET_MAC 必须在 INIT_CH395 之前) +## Trap 24:MAC 写入顺序违反`docs/芯片手册/CH395F.md` §9.2.1(SET_MAC 必须在 INIT_CH395 之前) ### 现象(静态审查发现;手册确证) -`net_init()` 原顺序为 `ch395f_init()`(CMD_INIT_CH395)→ `ch395f_set_mac_addr()`,与手册 §9.2.1 规定的初始化顺序**相反**。后果:INIT_CH395 已按 EEPROM 旧值加载 MAC,其后写入 EEPROM 的新 MAC 本次上电**不生效**(回落芯片内置/上次值,实际"滞后一次上电")→ UI 配置的 MAC 与设备实际在线 MAC 不一致,设备在线身份错误、网口排查困难。本轮未做"UI 改 MAC + 重启"台架复现,结论由手册条文确证。 +`net_init()` 原顺序为 `ch395f_init()`(CMD_INIT_CH395)→ `ch395f_set_mac_addr()`,与`docs/芯片手册/CH395F.md` §9.2.1 规定的初始化顺序**相反**。后果:INIT_CH395 已按 EEPROM 旧值加载 MAC,其后写入 EEPROM 的新 MAC 本次上电**不生效**(回落芯片内置/上次值,实际"滞后一次上电")→ UI 配置的 MAC 与设备实际在线 MAC 不一致,设备在线身份错误、网口排查困难。本轮未做"UI 改 MAC + 重启"台架复现,结论由手册条文确证。 ### 根因 -手册 §9.2.1「初始化 CH395,必要操作」顺序为: +`docs/芯片手册/CH395F.md` §9.2.1「初始化 CH395,必要操作」顺序为: ① `SET_MAC_ADDR` → ② `SET_IP_ADDR` → ③ `SET_GWIP_ADDR` → ④ `SET_MASK_ADDR` → ⑤ `INIT_CH395`。 - §5.7:`SET_MAC_ADDR` 把 MAC **写入芯片内部 EEPROM**(约 30us); - §5.13:`INIT_CH395` **初始化 CH395 的 MAC、PHY 以及 TCP/IP 协议栈**(约 5ms)。 @@ -766,4 +817,163 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报 `Drivers/BSP/NET/net_socket.c`(`net_init` SET_MAC 顺序)、`Drivers/BSP/NET/net_socket.h`(`net_init` 注释)、`Drivers/BSP/CH395F/ch395f.c`(四个 SET 的限定条件注释);`docs/网络/BSD_Socket_API_函数调用关系.md`(§4.1 / §7 顺序)。 ### 发现时间 -2026-10-04(静态审查项 P1-7;依据手册 §9.2.1 / §5.7 / §5.13 定案)。 +2026-10-04(静态审查项 P1-7;依据`docs/芯片手册/CH395F.md` §9.2.1 / §5.7 / §5.13 定案)。 + +## Trap 25:FTP 数据连接三连故障——丢 CONNECT 中断 / accept 竞态自毁缓冲 / 背靠背小段毒化 TX(N-30,2026-10-11 定案并修复,回归 4 轮 11/11) + +### 现象(生产 FTP 回归,多轮复现) +同一 FTP 会话内**第 2 次及以后**的数据连接(LIST/STOR/RETR 各一次)出现两类失败,两种面孔同一根因: +- **吞数据(SWALLOW)**:新数据连接建立成功(芯片 0x05/0x04 ESTABLISHED)、服务端 `net_send` 返回成功,客户端 20 s 收不到任何字节(无 RST、无 DISCONNECT,直到客户端放弃); +- **425**:新数据连接被接受后 ~10-20 ms 内出现 DISCONNECT(`chip tcp=0x00`),服务端 accept 失败 → `425 Can't open data connection`。(⚠ 本条当时的解读"芯片自杀连接"**有误**,见文末"三次定案"——实为客户端发完数据后的**正常 FIN** + 我方 accept 竞态自毁缓冲。) + +失败概率随**上一条数据连接的负载大小与间隔**变化(NAND 目录文件越多 LIST 越大越易触发),与提交历史无关——在 D20 基线 e5733b34 上以快速连发同样复现(D20 时代"11/11 通过"是慢速台架 + 干净目录的运气)。 + +### 定案实验(裸机芯片台架,剥离 OS/net/FTP) +实验分支 `n30-chip-bench`(worktree),`test/board/ch395f_reuse_bench.c` 在**内核启动前**直接调 `ch395f_*` 做受控矩阵:`{socket, close 后复开延迟, TX 长度, 关闭顺序}` 组合循环,每 50 ms 打印芯片两字节状态 + 中断寄存器;主机侧 `test/host/ch395f_reuse_host.py` 同步做纯 TCP 客户端。**OS/net/FTP 全部剥离后 bug 依旧复现** → 纯芯片操作时序问题,非上层逻辑、非芯片硬件缺陷。 + +关键现场(芯片日志): +``` +post-listen st=0x05/0x04 ← listen 返回成功,但 TCP 态停在旧连接的 ESTABLISHED(幽灵态) +unexpected int=0x10 while wait ← 只等来旧连接滞后的 DISCONNECT +no CONNECT within 10s ← 新连接的 CONNECT 中断永远没来(芯片状态却已是 0x04) +``` + +**芯片级规律**:CH395F 关闭连接后的拆链收尾窗口**变长且不可观测**——状态寄存器显示 CLOSED 持续 1 s 以上也不代表拆完(实测窗口可 >2 s 且时长不定)。窗口内重新 `open+listen`: +1. 新连接握手照常完成(状态寄存器 → ESTABLISHED)但 **CONNECT 中断被丢**(Trap 22 丢中断机制的窗口期形态)→ 事件驱动的 accept 永远等待 → 吞数据; +2. 或 listen 后状态直接假报 0x04(幽灵态),此时回读对端端口 = **旧连接端口**; +3. 或旧连接滞后的 DISCONNECT 打到新监听 → 触发 relisten 毁掉刚到的新连接 → 425。 + +### 已证伪的修复(全部经台架受控实验) +| 尝试 | 结果 | +|---|---| +| close 后固定沉降 100/300/800/2000/3000 ms | 仍挂(2 s 也挂过)——窗口变长,固定等待无保证 | +| Socket 轮换(sock3↔sock4 交替) | 仍挂——上一轮拆链尾巴跨 socket 毒化下一轮 | +| listen 后 close+重试 / 耐心等幽灵消退 | 持平对照组,无效 | +| 收发缓冲重分配(clear_recv_buf + set_*_buf) | 无效 | +| close 前 200 ms TX 排空延时 | 无效 | +| 禁用对账 tick(验证因果) | 仍挂——对账不是元凶 | + +### 有效修复(台架修复组 15/15,机制触发 5 次全部救回) +**双信源原则:中断只是快路径,状态寄存器才是真相。** 等连接时不能只等中断: +1. **accept 路径内联探针**(`net_lost_connect_probe`,`net_accept_locked` 每 100 ms 轮询挂探针):芯片状态已 ESTABLISHED 而 CONNECT 中断未到 → 连续 2 轮去抖后补做 accept(复用 `handle_connect_event` 簿记)。 +2. **幽灵态甄别**:补 accept 前回读对端端口(`GET_REMOTE_IPP`),**端口 = 上一条连接端口(`last_peer_port`)→ 幽灵,不 accept**——否则会把数据发给已死的旧连接(这正是生产 LIST#4"accept 成功但数据被吞"的直接成因:对账②曾把幽灵连接补 accept 了)。 +3. **对账②同步加幽灵甄别**(`net_reconcile_tick` 类型②分支同样比对 `last_peer_port`)。 +4. `last_peer_port` 登记点:`handle_disconnect_event` / `handle_timeout_event` / `net_relisten_try`(清零 remote_port 前转登)/ `net_close_locked`(跨 memset 保留——幽灵属芯片 Socket 级,与 fd 复用无关)/ NET_MSG_FIX 陈旧 ESTABLISHED 修复路径。 + +### 与 Trap 22 的关系 +Trap 22 的周期对账(200ms 节拍 + 400ms 去抖 + 1s 冷却)设计目标是"自愈兜底",对常驻监听器有效;但 FTP 数据通道是"活 1 秒就死"的短命监听器,且对账②**无幽灵甄别**——它既救不全(冷却期内 accept 已超时)又反过来帮凶(把幽灵补 accept)。Trap 25 把"看状态补 accept"内联进 accept 热路径(短去抖、无冷却)并补上端口甄别,对账②保留为兜底网。 + +### 对应文件 +`Drivers/BSP/NET/net_socket.c`(`net_lost_connect_probe` / `net_accept_locked`(含 closed-with-data accept)/ `net_reconcile_tick`② / `last_peer_port` 各登记点)、`Drivers/BSP/NET/net_types.h`(`last_peer_port` / `lost_conn_seen` 字段)、`Drivers/BSP/NET/lftpd/lftpd.c`(`send_list`/`send_nlst` 批量发送);台架工具 `test/board/ch395f_reuse_bench.{c,h}` + `test/host/ch395f_reuse_host.py`(实验分支 `n30-chip-bench` 注册编译,主分支只存档不注册)。 + +### 验证(台架层) +`ch395f_reuse_bench` 修复组(丢中断捕获自 accept)15/15,机制触发 5 次全部救回(对照同表 4/15 挂)。⚠ 生产回归当时未达 11/11——生产主导模式与台架不同,见下两节。 + +### 三次定案:所谓"kill 模式"是寄存器状态误读(2026-10-11 当日终案,双向验证) +第二轮验证(仅双信源探针修复,4 轮:1/11、5/11、2/11、2/11)暴露 STOR #2 仍 100% 失败,当时解读为"芯片在 accept 后 10-20 ms 杀连接"。后经**手册条文 + 网络资料双向核对**推翻: + +**寄存器读数本身全部正确**(`GET_SOCKET_STATUS_SN` §5.22:`05H=SOCKET_OPEN/00H=SOCKET_CLOSED`;第二字节 TCP 态 `00H=CLOSED/04H=ESTABLISHED`,与 WCH 官方数据手册表 5-8/5-9 一致;`GET_RECV_LEN_SN` §5.33 低字节在前,驱动实现一致)。**错在对 DISCONNECT 事件语义的解读**: + +1. `net_init` 的 `ch395f_set_fun_para(0x08)` 置 **FUN_PARA_FLAG_SOCKET_CLOSE=1**(§5.52 ②):产生 DISCONNECT/TIMEOUT 后芯片**不关 Socket、不清缓冲**,就是留给单片机把残留数据读出后再关 Socket。 +2. 判别探针现场:`kill-check recv_len=1027 re-read st=0x05/0x00`——Socket 仍 OPEN、TCP 已 CLOSED、**RX 缓冲 1027 字节 = STOR 载荷一个不少全在**。这是芯片按 §5.52 规格**保留现场**,不是杀连接。 +3. 那个 DISCONNECT 是**客户端的正常 FIN**:Python ftplib `storbinary` 语义 = `sendall` 发完立即关数据连接,然后回控制通道等 226——标准 FTP 行为。局域网小文件下"连接→发完→关闭"在 ~5 ms 内完成。 + +**425 真根因(竞态 + 自毁缓冲)**:客户端 5 ms 内完成发送并关闭;`cmd_stor` 的 accept RPC 赶到 net 层时 MCU 态已是 CLOSED;旧 `net_accept_locked` 把 CLOSED 一律当"无连接" → `net_relisten_try`(close+open **清空芯片 RX 缓冲**)→ 1027 字节被**我方亲手销毁** → WOULDBLOCK → lftpd 判"数据套接字已关" → 425。芯片全程无辜(此模式)。 + +**有效修复(已落地,`net_accept_locked` CLOSED 分支)**:CLOSED + auto_relisten + **芯片 RX 缓冲有数据**(`GET_RECV_LEN_SN`>0)→ 接受这条"已关但可读"连接(日志 `closed-with-data accept (drain pending)`),应用经 `net_recv_locked` 快路径(无状态门槛)排空,排空后 EOF 自然触发 relisten——完全对齐 §5.52 ②"读出残留数据后再关闭"的芯片契约。**验证:修复后 4 轮回归 STOR #2 从 4/4 失败 → 4/4 通过**(7/11、6/11、9/11、5/11,失败项中已无 STOR)。 + +**连带修正**:台架 mode0(主机先关)观察到的"accept 后 10-20 ms DISCONNECT"大概率同为误读——那是主机收完数据自己关的。"拆链窗口杀连接"理论对生产 425 不成立;台架**丢 CONNECT 中断模式**(主机侧 got=0 + 芯片 0x04 + 无中断,主机端独立证据)仍然成立,双信源探针修复保留。 + +**当时的遗留**:LIST #4/#7 超时(服务端 `net_send` 目录、客户端 20 s 收 0 字节、无 DISCONNECT)——发送方向的真问题,与上述 425 机制不同——已由下节"四次定案"解决。 + +### 四次定案:LIST 吞包 = 背靠背小段写毒化芯片 TX(2026-10-11 终案,修复后 8/8 通过) +探针定位过程(全部生产固件 + 临时探针,一轮复现): +1. PASV→accept 间隔 2 s(`net_listen_locked` open 重试在跑)→ 曾疑"listen 撞拆链窗口"; +2. accept 正常、CONNECT 正常、发送后中断位 `0x03`(SENBUF_FREE+SEND_OK)——**芯片自认发送成功**; +3. `GET_REMOTE_IPP` 对端 IP:port 完全正确(排除对端混淆); +4. **Socket 轮换**(服务端先关后 4 s 冷却、数据连接轮转 sock3~7):**证伪**——LIST 换到全新 sock5 仍 100% 挂(芯片行为与失败前完全一致); +5. **决定性对照**:同一目录同一载荷,LIST #1(会话首连,FAT 冷、readdir 慢、行间天然间隔大)通过;LIST #4/#7(FAT 热、每行一个 `net_send` 背靠背小段)必挂。 + +**定案**:`send_list`/`send_nlst` 每行一次 `net_send`(~60 B 小段、间隔仅 ms 级)——CH395F 对同一连接的**背靠背多段小写入**会把 TX 整条连接毒化(芯片报 SEND_OK 但对端协议栈全部丢弃,无 RST 无 DISCONNECT)。`net_send_locked` 的逐块协议(等 SENBUF_FREE 再写下一块)本身合规(§5.45),但芯片对该模式的内部处理不可靠;RETR 的 1 KB 满块连发不受影响(实测通过)。 + +**修复(`lftpd.c`)**:`send_list`/`send_nlst` 整份目录**一次性批量发送**(static 1536 B 缓冲,snprintf 累积 + 单次 `lftpd_inet_write`;溢出截断判 -1→550)。注意 `lftpd_inet_write` 成功返回**字节数**非 0(首版误按 0=成功判 → 全 550,已纠)。 + +**验证**:修复后 ftp_regression 连跑 **4 轮 11/11**(终版:批量发送 + closed-with-data accept + 丢中断探针 + 幽灵甄别,无轮换无探针)+ `ftp_auth_bench.py` **13/13** + `@test` 启动标记 PASS;此前同条件 LIST #4/#7 为 100% 失败、STOR #2 曾 100% 425。轮换冷却机制随证伪**已撤**(不留未证明的复杂度;其 5 轮 11/11 一并存档备查)。 + +**教训(Trap 25 总纲)**:本轮先后误读三次——把客户端正常 FIN 读成"芯片杀连接"、把 open 重试读成"拆链窗口"、把芯片假 SEND_OK 读成"数据已上线"。铁律:**CH395F 的"成功"(中断位/状态码)只代表芯片本地动作完成,不代表对端收到;判定必须回到对端可见证据(主机侧收发)+ 手册语义(FUN_PARA §5.52、状态码 §5.22)**。 + +**顺带发现的疑点(2026-10-11 已定案)**:`set_fun_para(0x09)` 的 bit0——§5.52 表明确写 **bit0 保留、必须为 0**,而 `net_init` 注释声称"bit0=1 多 Socket 模式"(手册与网络资料均无此说法,CH395 的 8 Socket 是芯片原生能力,靠 Socket 索引 `_SN` 寻址)。现固件 bit0=1 下 sock1/2/3 多路并行正常 → 该位被芯片忽略。**定案:改回 `0x08`**——官方手册 V1C/V1E/V2.2/V2.3 的 bit0 全是保留位,官方例程 bit0=0 下 8 个 Socket 照常可用(`EXAM5_TCP_Server` 传 0x02 却给 Socket 0~7 配缓冲),官方 FTP 例程本身就传 0x08。完整证据链见 Trap 18「定案更正」;**0x08 下的多 Socket 已台架实测通过**(2026-10-11 20:18,Phase 7 34/34,并发回显落在 sock2/4/5/6/7,N-31 已销项)。 + +### 发现时间 +2026-10-11(登记簿 N-30;裸机芯片台架定案实验见本节,实验分支 `n30-chip-bench`)。 + +--- + +## 2026-10-11 寄存器 / 数值口径全量审计(智能体团队,只读) + +**方法**:四路并行——驱动层逐命令(含官方 SDK 逐字对照)、NET/测试层逐调用点、Trap 01–12 复核、Trap 13–25 复核。三源交叉:`docs/芯片手册/CH395F.md` + WCH 官方例程 `CH395EVT/EXAM/PUB/CH395CMD.c`、`CH395INC.H` + git 历史。 + +### 干净清单(核对过、无异常) + +| 项 | 计数 | 结论 | +|---|---|---| +| `CH395F_CMD_*` 命令码 | 55 | 与 §5 命令表 + SDK `CMDxx_` 逐个一致,无一抄错 | +| 16/32 位量字节序 | 9 处 | 全部低字节在前,与手册明文 + SDK 逐字节一致 | +| 读命令输出字节数 | 14 条 | 无少读/多读(`GET_SOCKET_STATUS`=2、`GET_IP_INF`=20 且顺序 IP→GW→MASK→DNS1→DNS2、`GET_UNREACH`=8、`GET_REMOT_IPP`=6) | +| 常量表 | 60 个 | `ERR_*`18 / `TCP_*`11 / `SOCKET_*`2 / `PHY_*`6 / `PROTO_*`4 / `SINT_STAT_*`6 位号全对;唯一错值见 A3 | +| **`GET_GLOB_INT_STATUS_ALL` 拼装方向** | — | **无异常**:我们 `第1字节\|第2字节<<8` 与官方 `CH395CMDGetGlobIntStatus()` 逐字相同;`glob & (1<<(sock+4))` = §5.16 bit4..bit11 = SDK `GINT_STAT_SOCKn` | +| `get_socket_status` 缓冲区 | 17 处调用点 | 全部 `uint8_t st[2]`,无 1 字节残留(`db64e6ac` 引入 → `e5733b34` 修复,HEAD 无回归) | +| 中断判据 | 17 掩码 / 32 全局读 / 27 SINT 读 | 位号与 §5.16/§5.23 一致;只用 16 位 ALL 版 | +| 单位与取值范围 | 12 个参数点 | retrans 8/200 ms、KeepAlive 60000/5000(500 倍数、IDLE>INTVL)、ARP 100 ms 单位、MSS 1024 全部合规 | +| 缓冲布局 | 3 处组合 | `net_init` 8×(4收+2发)=48 块恰好用满、无交叠(零裕量) | +| 显示假象 | 10 处 `%d.%d.%d.%d` | 只有已定案的 `net_socket.c:1631` 是 MSB 重排假象,无新增同类 | + +### 新发现(去重后按严重度;已修 = 本次提交内) + +| # | 严重度 | 位置 | 问题 | 状态 | +|---|---|---|---|---| +| A1 | 中 | `test/board/ch395f_test_task.c:1005` | TC-203 的 Trap 14 反核实写成 `st_chk[0] == CH395F_TCP_ESTABLISHED`——byte0 只有 `00H/05H`,条件**恒假** → 该防线永久失效且日志无痕(与 Trap 07 同类:byte0/byte1 语义越界) | ✅ 已修(改 `byte0==OPEN && byte1==ESTABLISHED`) | +| A2 | 中 | 同上 `:1150` / `:1321` | Phase 3/4 把 `GET_RECV_LEN` 返回值直接当读取长度(本地 `rx_buf[300]`/`[64]`,芯片 RX 缓冲 2 KB)→ 栈溢出 | ✅ 已修(夹到 `sizeof` + 留痕) | +| A3 | 低 | `ch395f.h:253` | `CH395F_GINT_STAT_DHCPV6 = 0x8000`(bit15);SDK 是 `(1<<12)`、§5.16 明确 CH395F 的 `[12:15]` 保留 | ✅ 已修(改 `0x1000` + 注明本芯片恒 0;全仓零引用) | +| A4 | 低 | `ch395f.h:33-91`、`ch395f.c:484/506`、`ch395f.h:701` | 分组注释字节数错 4 处(`GET_GLOB_INT_STATUS_ALL`/`GET_UNREACH_IPPT` 归"1 字节输出"、`SET_FUN_PARA` 归"1 入 1 出"、`SET_RETRAN_PERIOD` 归"3 字节输入"、`GET_SOCKET_STATUS_SN` 归"1 字节输出");KeepAlive 参数注释写"秒"(§5.53/5.54 是 ms);`set_retrans_count` 写"0=默认值5"(§5.17 默认 12、无 0=默认语义)。**实现全部正确,注释误导**——Trap 25 的 1 字节缓冲栈越界正是这类注释发起的 | ✅ 已修 | +| A5 | 低 | `net_socket.c:238/1005/1080/1297/1666/708/3056` | 注释与手册/HEAD 相反:`FUN_PARA(SOCKET_CLOSE=0)`(实为 bit3=1,正是 closed-with-data accept 的契约依据)、"初始化成功后设置 MAC"(§9.2.1 ① 是之前)、"复用默认缓冲"(已 8 Socket 显式分配)、"布局由 MSS 推导"(固定布局、Trap 21 已解耦)、"等 SEND_OK"(门控是 SENBUF_FREE)、UDP 头 `[0-1] reserved`(§9.2.4 是数据长度) | ✅ 已修 | +| A6 | 低 | `ch395f.c:1176` | `write_send_buf` 超限**静默 return**(void、无日志),且上限取 DMA 2048 而非发送缓冲 1024;读路径有 OVERSIZE 金丝雀、写路径没有 | ✅ 已修(补 `[ERR]` 留痕 + >1 KB 告警,行为不变) | +| B1 | ~~中~~ **已推翻** | `net_socket.c:3553` | 原判"产品路径从未启用 Socket KeepAlive → 静默断开无人知晓"(§5.56 服务器侧应在 CONNECT 后开,产品三服务全是 Server,60s/5s/3 参数空转)。**2026-10-12 实测推翻前提**:连接静置时禁用 PC 网卡(真·静默死亡),芯片 **+2 s** 即报 `[ERR][NET] sock0 TIMEOUT` → 本层 `relisten quarantine` 释放重开;2 s 恰为 §5.17 重传 8×200 ms=1.6 s 量级,**与 KeepAlive 无关**(IDLE 最短 60 s)。链路层失效本就被快速覆盖。 | ✅ 已按 §5.56 补过使能并实测,**依 Trap 25「不留未证明的复杂度」撤回**,代码内留注释记录;残余场景见下「台架注入陷阱」 | +| B2 | 中低 | `net_socket.c:1638` | `ch395f_tcp_connect()` 返回值被丢弃(同文件 open/listen 都查)。§9.2.2 ⑧ 明文:`GET_CMD_STATUS` 返回 SUCCESS 仅表示**命令**执行成功、其他值表示**命令执行失败**,丢弃它 = 命令级被拒要等 30 s recv 超时才暴露 | ✅ 已修(2026-10-12):非 SUCCESS 且**非 BUSY** 才判失败——`poll_cmd_status` 超时返回的就是 `ERR_BUSY`(芯片仍在执行、命令未被拒),把它当失败会误杀慢芯片;明确失败 → `[ERR] rejected by chip: 0xNN` + 按 §9.2.2 ⑧"从 ⑤ 重开"关 Socket 回滚 + 返 `-1`。**Phase 7 实测回归**:TC701 回显 64 B 校验 PASS、TC702 重连 ×3 全 PASS(正常连接无误杀);不可达对端时芯片回的是 SUCCESS、`ret=0` 且不打 `rejected by chip`,失败仍由 `CONNECT TIMEOUT`(~1.6 s) 裁定 → 行为未变。⚠ 残余:`net_reconcile_tick` 仍只裁 ESTABLISHED/LISTENING,`CONNECTING` 丢中断无人收口(产品不走出向连接、无法实测,按"不加未证明复杂度"暂不动);TC703 断言 `rc==0` 把"返回值被丢弃"写进了判据,真发生命令级被拒时它会 FAIL(属正确告警) | +| B3 | 低 | **两处**:`net_socket.c:1120-1125`(`net_listen_locked`)+ `net_socket.c:2060-2065`(`net_relisten_locked`,审计原只登记前者,**后者才是每次断连的热路径**) | 两处对**全部 8 个 Socket** `GET_INT_STATUS_SN`(读即清)+ 无条件读清全局。机制:§5.15 明文「此命令执行完毕后…将全局中断状态清除」= 全局寄存器**整体读即清、无按位清除手段**,而 `net_poll` 只在**全局位置位时**才去读该 Socket 的 SINT(`:469` 门控)→ 丢弃式读全局把别的在用 Socket 的门一并焊死;SINT 又**粘滞到被读**(Trap 14 实证 close/open 都不清 SINT)→ 全量读走即**永久销毁**其 CONNECT/DISCONNECT/TIM_OUT/SENBUF_FREE(唯 RECV 是条件位可下轮补,Trap 08)。git 考古:`e862009bc`(2026-07-19 多连接时代,无注释引入,同提交还删掉了 net_poll 里"勿在此读 per-socket 中断"的警告) → `319d00e3` 原样搬进单连接 → `db64e6acf` 复制进 relisten;**不属于任何已登记 Trap 的修复**(Trap 14/17/23 指定的预清都在别处且是单 Socket)。官方 SDK 21 个例程一律"全局位图门控 + 单 Socket 读 + 立刻转存",open/listen 前后**不清任何中断**,无一例全量读 8 个 SINT | ✅ 已修(2026-10-12):两处收窄为**只清本 Socket 的 SINT** + **删除丢弃式全局读清**(全局交 `net_poll` 集中读清,与 AGENTS 模块约定「所有 CH395F 中断在 net_poll 内集中读取并清除」一致);本 Socket 残留的全局位被 `net_poll` 读到时 SINT 已为 0,无害了结。**两路子 Agent 曾分歧**:一路建议保留全局读清("只搁浅不销毁"),一路警告"保留=修一半"——采后者,芯片无按位清除,保留即热路径继续焊门。**回归**:`@build` 0/0 `Code=197008`(−24 B);`@test` PASS;Trap 25 `ftp_regression` **11/11 ×3**;Trap 23 `ftp_abort_probe --check-abort` RST **5/5**(450) + FIN **5/5**(226);Trap 22 `:21` 0ms×50 **50/50**、100ms×50 **50/50**、`:8888` 0ms×20 **20/20**(均与文档基线一致);`:7003` 腿见 B10(A/B 已证明与本改动无关;B10 修完后该腿回到 **10/10**)。⚠ 残余:`net_connect_locked:1601` / `net_close_locked:2571` / `net_relisten_locked:1986` 三处无条件全局读清仍会**搁浅**(延后、非销毁)其它 Socket 挂起事件——它们是 Trap 14/17/23 的指定两级预清,本次不动,宜与 B4 一并处置 | +| B4 | 低 | `net_socket.c:446/469` | 全局中断 bit0-3(`UNREACH`/`IP_CONFLI`/`PHY_CHANGE`/`DHCP`)被读清但从不消费;`ch395f_get_unreach_info()` 全仓零调用 → IP 冲突、ARP 不可达、链路抖动三类事件完全静默 | ⏳ 待修(可观测性缺口) | +| B5 | 低 | `net_socket.c:3878-3881`(`net_getsockname`,函数 :3851-3887;原登记 :3849-3854 已陈旧)→ `lftpd.c:803-809` | `net_getsockname` 的 `s_addr` 组装方向与本层约定(`net_inet_addr`/`net_inet_ntoa`)相反 → FTP 日志把设备自身 IP 打成 `100.1.168.192`。⚠ 同一 `getsockname` 的 PASV 227 消费点(`lftpd.c:497-503`)做了 `net_htonl` 再 MSB 拆,**双重反转恰好正确,勿误判**;风险前瞻:若把该 `s_addr` 喂给 `net_connect`/`net_sendto` 就会连到反序地址 | ⏳ 待修(改一处须同时核对 PASV) | +| B6 | 低 | `net_socket.c:591/1225/3608` | 3 处状态判据只看 `byte1`、未按 §5.22 用 `byte0` 门控;reconcile 的 else-if 链里 `(00H, 陈旧 04H)` 会先命中"补 accept"而非"重建监听" | ⏳ 待修(Trap 07 实测 `byte0=00H` 时 `byte1` 读回 `0x00`,故为低概率) | +| B7 | 低 | `ch395f_test_task.c:1379/1504` | Phase 5 单次写 4093 B 超 1 KB 发送缓冲与 2048 DMA 载荷 → 整块被丢;注释仍写"4096 B = 发送缓冲上限"(2026-09-22 布局解耦前的旧事实)。A6 修后会打 `[ERR]` 而非静默 stall | ⏳ 待修(chunk 改 ≤1024 或分块) | +| B8 | 低 | `ch395f_test_task.c:1290-1292` | Phase 4 把 sock6 缓冲重排到块 24-27,与 `net_init` 固定布局中 sock4 的 24-27 **交叠**(§5.45 禁止) | ⏳ 待修 | +| B9 | 低 | `ch395f_reuse_bench.c:93`、`net_socket.c:39`、`net_socket.c:262` | bench 用 `get_phy_status() != 0` 判链路(01H=断开也成立,`net_task.c:122` 的 `!= CH395F_PHY_DISCONN` 才对);`NET_PHY_CHECK_INTERVAL_MS` 死宏;无芯片版本门控(§5.16 <0x44 不支持 ALL 版、§5.52 bit3/4 需 ≥0x46,实测 0x4A 无影响) | ⏳ 待修(前两项死代码/卫生) | +| B10 | **中** | `net_reconcile_tick` 认领范围 + `handle_disconnect_event` 释放分支(`net_socket.c:3663-3686`) | **孤儿监听 → 端口永久拒连**。断开时释放分支把状态置 `CLOSED` 并注释「留给 `net_recv_locked` 把剩余数据读出来再关,**等待应用重新监听**」——即由应用负责重建;但 `ui_net_sink` 只在 `net_recv` 返 0(EOF)时才清 `g_client`,而 EOF 已被这个事件分支消费掉 → **net 层等应用重建 / 应用等 net 层给 EOF / 对账只裁 ESTABLISHED·LISTENING 直接跳过 / 芯片不会自己回 LISTEN**,四方互等。实测(临时探针):`TEMP orphan sock2 state=0 port=7003 chip=0x05/0x00` 每秒一条持续 12 s+,`:7003 300ms×10` **0/10**、+30 s 仍拒连。与 Trap 22 的关系:Trap 22 的①③两类都要求本层处于 ESTABLISHED/LISTENING,**孤儿态恰好落在两类之外**,所以那套对账全程沉默 | ✅ 已修(2026-10-12):`net_reconcile_tick` 新增**类型④**认领孤儿(`in_use` + `state==CLOSED` + `auto_relisten` + `type==STREAM` + `local_port!=0`,芯片侧不在 LISTEN)→ 复用 `net_relisten_try`(自带"已在重试队列则不重复登记"保护,与 D10 重试队列、Trap 23 沉降隔离兼容)。⚠ **`auto_relisten` 是决定性门**:少了它,"本层 CLOSED + 已绑端口"的**出向连接**(板级 Phase 7 TC701~703 用 `net_connect` 绑本地口)会被误判孤儿并被 relisten 成监听器。**回归**:`:7003 300ms×10` **0/10 → 10/10**(+20 s 存活);`:21` 0ms×50 **50/50**、100ms×50 **50/50**;`:8888` 0ms×20 **20/20**;`ftp_regression` **11/11 ×2**;Trap 23 RST **5/5**、FIN **5/5**;`@test` PASS(600 ms、回读无差异);`Code` 197008→197084。⚠ **残余(未根治,已立项根治)**:生产者仍在——释放分支依旧把状态停在 `CLOSED` 等应用重建,④是**兜底**(实测救援后端口 1 s 内恢复,日志 `reconcile: orphan listener … -> relisten` 可见)。⚠ **④ 有副作用**:`net_recv` 的 EOF 是**边沿**(只在 recv 那一刻 `state==CLOSED` 才给 0,`:1880-1888`;已被别处 relisten 回 LISTENING 则恒 `WOULDBLOCK`,`:1890`)→ ④ 抢先 relisten 会把慢消费者的 EOF 永久吞掉,`g_client` 不清、新客户端数据被旧会话静默吸收。**原理与分阶段根治方案 → `docs/子系统设计/网络Socket监听生命周期设计说明.md`**(含:芯片层为何永不自动回 LISTEN、本仓 `fun_para=0x08` 使 `0x05/0x00` 成为合法常驻态、RFC 1122 §4.2.2.18 与 POSIX accept 的规范冲突、12 个重建所有者与 U1~U15 无人负责组合、以及"芯片原生多连接模式(A′)为何对本产品不可用"的决定性证据) | + +### 文档侧待修清单(本轮只改了最要紧的几处,其余登记在此) + +- **最高优先级**:Trap 13 的"部分读后 `recv_len` 归零"与 §5.34 直接冲突(已在本条加复核注),但**同一机理仍被当作现行约束**写在 `CH395F_Test_Guide.md:28/69/930/932/958`、`docs/网络/BSD_Socket_API_函数调用关系.md:196/362`、`net_socket.c` 两处注释里——须与 Trap 21 的撤回同批处置,并补一条"部分读 → 再查 `recv_len`"的定向 TC。 +- 陈旧数字:`CH395F_Test_Guide.md:14/24/645/660/1141/1200`(DMA 4100 / 载荷 4096,现 2052/2048)、`:72/1201`(`NET_DMA_MAX_PAYLOAD=4096`,现 2048;实际分块上限是 `NET_SEND_CHUNK_MAX`=1024)、`:1020-1040`(TC-NET-1002/1003 期望 4096/4097 单次 DMA)、`docs/网络/BSD_Socket_API_使用指南.md:1246`(`NET_SEND_BUF_SIZE` 宏已不存在)。 +- 悬空引用:`Trap 19` 从未存在(`Test_Guide.md:878` 仍引用);`Test_Guide.md:353/418` 的"见风险 #4 / 风险表 #1"指向已删除的表。 +- 口径冲突:`Test_Guide.md:1135` 的 TRAP-07 行仍写"卡 FIN_WAIT_2 / `close_socket`(RST)"(与 Trap 07 定案更正冲突);`:45` 的"TC704 40/40"与 `:880` 的"`PHASE7_MULTI_COUNT` ≤5"矛盾(HEAD 宏值仍是 8);`Drivers/BSP/NET/README.md:35` 与 `net_socket.c:1864` 的"正常 FIN 路径零改动"与 Trap 23 二阶段矛盾。 +- 机理措辞:Trap 17"等**对端** FIN/ACK"应按 Trap 07 实测改写(自身 FIN 的 ACK + 本地 TIME_WAIT,0~150 ms);Trap 20"硬件发送 FIFO 1 KB"应改"本仓 2 块分配的发送缓冲区"(§5.45 上电默认是 4 块=2 KB)、"等 `SEND_OK`"应为 `SENBUF_FREE`;Trap 25 的 `NET_MSG_FIX` 应为 `NET_MSG_RESYNC_LISTEN`、`GET_REMOTE_IPP` 应为 `GET_REMOT_IPP`、"§5.45"应为 §5.32+§5.23①。 +- 已失效条目未标注:Trap 01/02/11 的多连接模型随 2026-08-25 `319d00e3` 删除;Trap 09 的 `osDelay(7000)` 已被就绪屏障取代;Trap 10 的"`net_connect()` 不使用消息队列"已失效;Trap 12 的 #2/#4 与仓内 FatFs 源码矛盾(`ff.c:3101-3109` 剥前导分隔符、`:3127-3129` 空名返回根目录),且 `FF_FS_RPATH=0` 下 `"."` 不是合法路径段(HEAD 用 `"0:"`)。 +- 日期:Trap 01/02/03/04/06/07 的"2025-07"均早于仓库首提交 `b1d05091`(2026-06-05);Trap 03/04/06 的 Phase 编号也错(DHCP=阶段 4、UDP=阶段 3、KeepAlive=阶段 11)。 +- 证据通则:`*.log` 在 `.gitignore` 内 → 本文所有台架计数(40/40、34/34、15/15、4 轮 11/11、13/13…)**脚本在库、日志未入库 = 不可复核**;Trap 25 引用的实验分支 `n30-chip-bench`(tip `eddbede`)不含 bench 源文件与 uvprojx 注册,裸机芯片台架无法从任何已提交状态重建。 + +### 主机侧测试工具过期(2026-10-12 实测) + +- **`MDK-ARM/ftp_abort_probe.py` 已修**:`raw_login` 用 `USER anonymous`、`followup_ok` 用 `login("anonymous","x")`,N-19 后匿名被 530 拒 → Trap 23 两条腿**假 0/5**(round 1 报 `RuntimeError: PASV failed`,且这条半认证会话占死 FTP 唯一会话,导致后续轮全变 `ConnectionRefused`)。改 `admin/admin`(两处)后 RST **5/5**、FIN **5/5**,与 `:779`/`:781` 基线一致。**教训:跑既有回归腿前先确认凭据仍有效,否则会把工具过期读成固件回归。** +- **同族未修**:`test/host/ftp_up_dl_test.py`(:123-126)、`ftp_speed_test.py`(:116-121)、`ftp_test.py`(:93-99)仍用匿名登录 → 对当前固件必挂登录步;`docs/测试说明/FTP_Test_Guide.md` 推荐的这几个入口因此已失效(现行可用入口是 `test/host/ftp_regression.py`,默认 `admin/admin`)。 + +### 台架注入陷阱(2026-10-12,B1 复核过程中踩到) + +- **Windows 出站防火墙规则不能用来模拟"对端静默死亡"**:WFP 对**已建立**的连接有状态缓存,中途 `New-NetFirewallRule -Direction Outbound -Action Block -RemoteAddress <设备IP>` 不影响既有流——实测静置 + 阻断 250 s 芯片毫无反应,看似"KeepAlive 不生效",实则 ACK 一直在正常往返。 +- 更糟的副作用:该规则会**吃掉本机后来发出的 FIN** → 设备侧留下一条**幽灵 ESTABLISHED**,之后所有新连接被 §5.29"向试图连接的远端发送 TCP RESET"直接 RST,表现为"端口拒连",容易被误判成设备故障(实为自伤)。撤规则后设备自行恢复。 +- **能真正掐断链路的手段**:`Disable-NetAdapter`(链路真的 down)。⚠ 本机 PowerShell 的该 cmdlet **只有 `-Name` / `-InterfaceDescription`**,没有 `-InterfaceIndex`/`-ifIndex`;网卡名是中文,用 `(Get-NetAdapter -InterfaceIndex 19).Name` 取值再传入,避免命令行里写中文字面量。禁用/恢复必须写在 `try/finally` 里,否则网卡留在关闭状态。 +- **仍未覆盖的场景**:`Disable-NetAdapter` 只能造"链路层失效";"**链路仍在、对端静默**"(NAT 会话过期、中间节点黑洞)本轮没有安全注入手段(静态 ARP 黑洞会被交换机泛洪兜底、删 IP 会连带拆掉本机连接)。这才是服务器侧 KeepAlive 唯一可能的价值场景,**未证实**,故不保留该改动。 +- **主机侧回显服务器收不到入站连接 → Phase 7 全量 `CONNECT TIMEOUT`(2026-10-12 实测)**:本机对 `C:\programdata\anaconda3\python.exe` 有两条 **Public 入站 Block** 规则(`C:\python314\python.exe` 才是 Allow),而 `.venv\Scripts\python.exe` 的基座解释器正是 Anaconda python(构建报错自曝 `C:\ProgramData\anaconda3\python.exe: can't open file ...`)。Windows 防火墙**块规则优先于放行规则** → 中途加的 `New-NetFirewallRule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8081 -Profile Any` **完全无效**(实测两轮全灭),只有 `Set-NetFirewallProfile -Name Public -Enabled False` 才通。症状极具误导性:芯片 ~1.6 s 报 `CONNECT TIMEOUT`、`net_connect` 仍返 0、`[CONN] openOK` 照常打印,看着像芯片/驱动故障,实为主机防火墙。固定跑法:`try { 关 Public → 复跑 } finally { 开 Public }`(或改用 `C:\python314\python.exe` 起服务器)。 +- **Phase 7 的两条既有失败与改动无关,别追**:① TC704 `socket create #5` FAIL = E-23(`NET_MAX_SOCKETS=8` 减 3 个常驻监听 → 并发上限 5,而 `PHASE7_MULTI_COUNT` 仍是 8);② TC703 要求 PC 服务器**离线**,与 701/702/704 环境互斥,同轮连跑必有一侧失真(本轮服务器在线时它反而 PASS,只因芯片先 `CONNECT TIMEOUT`)。 +- **判读教训(与 Trap 25 同源)**:看到 `TIMEOUT` 先**算时序再归因**——2 s 与 60 s 一眼可分;否则会把"重传超时"误记成"KeepAlive 生效",把相关性当因果。 +- **A/B 归因前必须每腿硬复位(B10,2026-10-12)**:`:7003` 这类腿一旦把监听打死就**不自恢复**,下一腿是接着死状态跑的 → 对照组失真。本次第一腿(基线 1/10)就是这么废的;正确做法是**每个样本前 `stlink_flash.py reset` + 等 12 s 干净启动**,并取多样本(本轮基线 5/1/1/5 vs 改动 3/7/2/1,均值 3.0 vs 3.25 → 统计不可区分,才敢判"与改动无关")。 +- **`[PROF]`/`[PROF2]` 字段不是计数器,别拿来判"任务卡死"(B10)**:`netp` = `net_poll` **段最长耗时(µs)**、`netn` = 采样瞬间在段内的时长(`App/util/task_prof.c:172`)→ 数值恒定只说明"没刷新最大值"。真正的停摆信号是 `[ERR][WD] task stall`;且其 `miss=` 打印的是**累计** `s_miss_total`(不是连续缺席数,连续数封顶在容忍值 8),容忍 = 8 轮 ×100 ms = **连续 800 ms 有任务缺席即停喂**、IWDG 2.5 s 溢出(`App/util/task_watchdog.c:466-492`)。缺席位图解码:`req=0x77` 为 DEFAULT/NET/FTP/UI/TIMESYNC/ADC,`mask=0x41` → 缺席 NET+FTP+UI+TIMESYNC 而 DEFAULT/ADC 正常 = "netTask 卡住、所有做 `net_*` 的任务阻塞在 `osMessageQueuePut(osWaitForever)`+`xTaskNotifyWait(portMAX_DELAY)` 后面"的指纹。 +- **静默黑洞用临时探针抓状态,测完即删(B10 手法)**:B10 的孤儿态在日志里毫无痕迹,靠在对账跳过分支加一条 `TEMP orphan … state=%d port=%u chip=0x%02X/0x%02X` 才直接看到 `state=0 / chip=0x05/0x00`;同一轮还顺手用探针**否掉了自己的第一假设**(怀疑 `net_listen_locked` 的 `osDelay(500)` 冻结 netTask → 探针 0 次命中)。假设写盘前先测,比推理便宜。 diff --git a/docs/问题记录/错误码传播审计.md b/docs/问题记录/错误码传播审计.md index e73a769d..f098233a 100644 --- a/docs/问题记录/错误码传播审计.md +++ b/docs/问题记录/错误码传播审计.md @@ -10,7 +10,7 @@ > **本版**:2026-09-25 重写 —— ① 已闭环项从正文移除,只在 §2 留一行索引;② 全部未闭环项 > **逐项回到当前代码复核**;③ 新增本轮复核发现。 > **代码基线**:`v2-on-ui` @ `9a8255f`(2026-09-24),复核日期 2026-09-25 -> **配套文档**:`docs/待处理内容.md`(N-2/N-4 登记项)、`docs/网络/Net层阻塞模型架构设计.md`(E-02 架构现状)、 +> **配套文档**:`docs/任务规划/README.md`(N-2/N-4 登记项)、`docs/网络/Net层阻塞模型架构设计.md`(E-02 架构现状)、 > `docs/问题记录/*_Trap_Records.md`(模块陷阱史)、`AGENTS.md`(已固化铁律) --- @@ -60,7 +60,7 @@ > > **2026-10-03 变更**:**E-34 / E-04 已修复并通过真机验证**(CH395F 字节命令有限超时 + `remo_send` 返回值校验;见 §5 第一档)。 > **2026-10-03 变更②**:**GD5F 错误通道统一(E-10 / E-42 / E-53 / E-54 / E-56 / E-57)已修复并通过台架套件回归**(`=== GD5F2GQ5UE Tests: 16/16 PASSED (failed=0) ===`,只读模式;见 §5 第二档 #1)。 -> 真机全闭环 `@test` PASS 7/7;执行判据见 `测试说明/实机测试实施指南.md` §3 / §10。 +> 真机全闭环 `@test` PASS 7/7;执行判据见 `docs/测试说明/实机测试实施指南.md` §3 / §10。 --- @@ -196,7 +196,7 @@ E-57(`int` / `int32_t` 混用)、**E-10 / E-42 / E-56(GD5F init / BBT / ra - **验证**:构建 0 错误 0 警告;cppcheck(warning/performance/portability)无告警; `git diff --check` 无空白问题;编码无 `U+FFFD` 且 BOM 状态与原文一致 - **代码代价**:BSS +64 B(8×(4+4)) -- **登记**:`docs/待处理内容.md` N-5(✅ 已销项,2026-09-26 清理) +- **登记**:`docs/任务规划/README.md` N-5(✅ 已销项,2026-09-26 清理) - **残留**:`net_recv_to` 继续用返回码 `-2` 表达超时(不依赖 errno),保持既有正确做法 --- @@ -455,7 +455,7 @@ E-57(`int` / `int32_t` 混用)、**E-10 / E-42 / E-56(GD5F init / BBT / ra | **E-23** | `test/` | 测试套件缺代次检查(accept 失败分支);TC704 并发度偏高 | **仍然存在** | 加代次断言;TC704 降并发或在测试构建抑制服务任务 | | **E-30** | `simulator/sim/net_socket_sim.c` | sim 后端 errno 语义与 MCU 不对齐(成功即清 errno,MCU 不清) | **仍然存在**(语义差异在 §6 表中仍成立) | 对齐语义;并在缺陷记录中标明"sim 复现盲区" | | **E-57** | `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h:119/140/189/222-253` | 公开 API 返回类型 `int` / `int32_t` 混用(码值均为小负数,无功能影响) | ✅ **已修复(2026-10-03)**:公开 API 全部统一为 `int32_t` | 见 §5 第二档 #1 | -| **E-60** | `Drivers/BSP/SD2506/sd2506.c/h` | `SD2506_CHARGE_5K` 宏位与手册 §4.8 不符的风险 | **已订正**(2026-09-21,`41e111b`):宏位已改为 `bit1`,18H 回读校验在位 | 无需改动,仅作为"手册总表 vs 专用表不一致"的案例保留 | +| **E-60** | `Drivers/BSP/SD2506/sd2506.c/h` | `SD2506_CHARGE_5K` 宏位与 `docs/芯片手册/SD2506API-G.md` §4.8 不符的风险 | **已订正**(2026-09-21,`41e111b`):宏位已改为 `bit1`,18H 回读校验在位 | 无需改动,仅作为"手册总表 vs 专用表不一致"的案例保留 | | **E-61** | `Drivers/BSP/SD2506/sd2506.c:482` | `sd2506_clear_int` 注释称"ARST=1 时自动清除 INTAF",实际 ARST 在 **CTR3**(11H)而代码从未写过 CTR3 | **仍然存在(纯注释错误)** | 修注释:明确"ARST 未启用,INTAF 靠读 CTR1 清除" | | **E-62** | `Lib/FatFs/ffconf.h` | `FF_USE_LFN=0` → 严格 8.3 短名;自愈逻辑若把 `FR_INVALID_NAME` 归入损坏触发集 → 不可逆重格式化(`.fschk` 无限重格式化事故根因) | **铁律已固化**(`AGENTS.md`);`ff.c` `create_short` 全部拒绝点已加 DEBUG 打印 | 无需改动,回归监督项(与 §3.2-E-31 后续同族) | diff --git a/docs/问题记录/项目审查与优化建议报告.md b/docs/问题记录/项目审查与优化建议报告.md index 1055755b..4b114413 100644 --- a/docs/问题记录/项目审查与优化建议报告.md +++ b/docs/问题记录/项目审查与优化建议报告.md @@ -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。 +> **与既有文档的关系**:本报告不重复 `docs/任务规划/README.md`(逐条缺陷台账)与 `docs/内存管理/RAM使用分析与优化方案.md`(内存专项方案),而是做**体系层评审 + 量化补强 + 新增发现**。新增发现统一编号 `N-xx`,已在台账中的条目按 `P1-x/P2-x/P3-x` 引用。 +> +> **销项约定(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/对时服务 | ❌ 一行未改;⚠ 且**不在 `../任务规划/README.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,36 @@ --- +## 0.4 未闭环清单(2026-10-11 复核) + +> 只列**未办 / 半办**项;已修项已按「修复即销项」删除(去处见变更记录)。图例:❌ 未办 / 🟡 半办。 +> **统一排序与推进**:本表各项已于 2026-10-11 并入 [`../任务规划/README.md`](../任务规划/README.md) §2 与 [`../任务规划/登记簿.md`](../任务规划/登记簿.md) +> (新编号 N-19~N-29 + U 簇,出处列保留本报告编号);**顺序权威在 `docs/任务规划/README.md` §2,本表为评审快照**。 +> ⚠ 本报告编号与登记簿同名不同义(报告 N-16 = 日志逐条开关,登记簿 N-16 = A2/A4 负向台架)→ 引用带文档名。 + +| 类别 | 编号 | 未闭环内容 | 下一步 | +|------|------|-----------|--------| +| **安全** | **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 ✅ | 三件套已建(2026-10-12):`scripts/ram_baseline.tsv` + `docs/内存管理/RAM预算台账.md` + `mem-budget` 钩子(92% + Top-10 附表) | 已闭环;R-9/R-2 排期解锁见登记簿 N-27(W3) | +| **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 +116,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 +128,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 +171,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,57 +187,37 @@ 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/任务规划/README.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
FS挂载+配置引擎属主] - B --> T2[netTask 1024w
net_poll+消息串行] - B --> T3[uiTask 4096w
LVGL+UI sink] - B --> T4[ftpTask 1024w] - B --> T5[timeSyncTask 512w] - B --> T6[testTask 1024w
产品构建也在跑] - 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 做得好的(应保留并显式制度化) 1. **文档密度与可执行性**:每篇都带"步骤 + 判据"(如 UI 制作流程有 P0→P4 Gate);测试说明按模块拆分并给出预期串口标记,可直接接 `@test`。 -2. **陷阱台账**:`docs/问题记录/*_Trap_Records.md` + `docs/待处理内容.md`(P1-1..P3-13,带 ✅ 状态与日期)——这是本工程最有价值的资产,多数嵌入式项目没有。 +2. **陷阱台账**:`docs/问题记录/*_Trap_Records.md` + `docs/任务规划/README.md`(P1-1..P3-13,带 ✅ 状态与日期)——这是本工程最有价值的资产,多数嵌入式项目没有。 3. **图与脚本同提交**:`tools/arch_diagram/`、`tools/refgraph/` 生成器 + 产物入库,图可再生而非手绘。 4. **UI 设计资产链完整**:原始参考图 → 说明 md → 代码,可追溯。 @@ -218,72 +232,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`;两表均改为**受管生成区**(锚点 ``),`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 +258,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 +272,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 侧背压 | -| 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);⚠ 且**未登记进 `docs/任务规划/登记簿.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 +289,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 +328,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 +345,143 @@ 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`);`docs/任务规划/测试与架构专项.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 残余**)。 + → ✅ **2026-10-12 闭环**:新增 `mem-budget` 钩子(`scripts/ram_baseline.py --gate`)——RW_IRAM1/RW_CCM >92% 即 FAIL + Top-10 RW+ZI 附表;无 `.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 趋势表)。 + → ✅ **2026-10-12 闭环**:`scripts/ram_baseline.tsv` 已建(`--update` 每次 @build 后追加;`--trend` 高亮 RW+ZI 增长 >1 KB 对象)。 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 后半段无法排期。 + → ✅ **2026-10-12 闭环**:`docs/内存管理/RAM预算台账.md` 已建——子系统配额(网络 12/存储 12/UI 16/RTOS 堆 48/应用 8 KB,现状超配 = W3 债务如实展示)+ **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 +491,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 +501,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` 的 ``,消费者 TU 会报 `cannot open source input file`(AGENTS.md 2026-10-10 记录),这类报错落在**别的文件**上、易误判,仍不等于"存在但未注册"的检查。 + ### 6.3 标准化目标:四层 + 一个闭环 ```mermaid @@ -608,6 +562,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 +584,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 `(已有 `--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 +596,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 +616,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 轮询) | 队列占用可测量下降 | -| 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 | ✅ **2026-10-12 已建**(`ram_baseline.py` 三模式:`--gate`/`--update`/`--trend`/`--write`;另有 `mem-budget` 钩子) | +| `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 `、`: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 +688,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 移植缺陷)** 既未修复也未登记在 `../任务规划/README.md` → 建议补登。 +> - ⚠ **编号撞车**:本报告曾用 N-18 指"无 CI"(该条已销项删除),而登记簿的 N-18 = "8 路电气量分属两片转换器(V0.2 改版项)"。引用时一律带文档名。 +> - ✅ `docs/内存管理/RAM预算台账.md` 与 `scripts/ram_baseline.tsv` **已于 2026-10-12 创建**(见 §5.5 各条闭环标注)。 +> - 本报告按 `docs/README.md` 的定位是**体系层评审报告,不是待办池**——待办一律以 `../任务规划/README.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` §6.2 / §8(v1.13) | +| 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 及理由) | diff --git a/scripts/README.md b/scripts/README.md index 6b7ee36f..37eb6f0e 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -11,7 +11,8 @@ |---|---|---| | `check_coding_standard.py` | `C coding standard` | C 代码规范机械检查(命名前缀/常量右置/头文件保护宏等) | | `check_mojibake.py` | `Forbid U+FFFD mojibake` | 扫描 `U+FFFD` 乱码(`EF BF BD` 字节) | -| `check_doc_links.py` | `doc-links` | **文档链接存在性** + 命名规范(D-3/D-7) | +| `check_doc_links.py` | `doc-links` | **文档链接存在性** + 命名规范(D-3/D-7)+ **`§N.M` 章节引用**(口径见 `docs/文档规范.md` §3.5)。钩子已带 `--sections-strict`(自引用失效也拦);`--section-todo` 列出机器判不了目标的 `§`(带改写建议)。每次跑打印「可判定比例」行,防覆盖率回落 | +| `section_aliases.json` | (被 `check_doc_links.py` 读) | 文档**昵称别名表**:「内存方案」→ `docs/内存管理/RAM使用分析与优化方案.md`。只登记无歧义昵称;登记一次即把该昵称的引用纳入硬门禁 | | `gen_task_table.py` | `task-table` | FreeRTOS 任务表以 `Src/freertos.c` 为**唯一事实源**生成/校验(D-2/D-7) | | `check_mem_numbers.py` | `mem-numbers` | 内存/Flash 数字以 `.map` 为唯一事实源校验(D-7 体积档) | | `check_test_suite.py` | `test-suite-switch` | 测试套件开关=产品构建(防测试固件静默提交) | @@ -23,6 +24,14 @@ | `ram_report.py` | 从 `.map` 生成**静态 RAM/Flash 占用**报告(→ `docs/内存管理/data/`) | | `stack_report.py` | 从 Keil 静态调用图 + 串口 `[STACK]` 水位生成**每任务最坏链**报告 | +### 引用同步(改名 / 挪目录) + +| 脚本 | 作用 | +|---|---| +| `rename_ref.py` | **改名/挪目录 + 全库引用自动改写**:默认 dry-run 预览,`--apply` 才落盘(`git mv` → 改写引用 → 自动跑 `check_doc_links --all` 复核)。覆盖反引号引用、Markdown 链接、图片 `../` 层级换算、`#include`、`uvprojx`/`CMakeLists.txt` 源列表。固定动作与边界 → `docs/文档规范.md` §3.6 | + +> `check_doc_links.py` 除链接存在性外,还校验 **`§N.M` 章节引用**是否指向目标文档真实存在的编号标题(口径见 `docs/文档规范.md` §3.5)。 + ### CI 与本地等价(2026-10-01 T-6) | 脚本 / 文件 | 作用 | @@ -43,6 +52,8 @@ # 单个脚本 & '.venv\Scripts\python.exe' scripts/check_doc_links.py --all +& '.venv\Scripts\python.exe' scripts/rename_ref.py docs/旧/名.md docs/新/名.md # 预览 +& '.venv\Scripts\python.exe' scripts/rename_ref.py docs/旧/名.md docs/新/名.md --apply # 落盘 & '.venv\Scripts\python.exe' scripts/gen_task_table.py --check # 或 --write / --diff & '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --check & '.venv\Scripts\python.exe' scripts/check_test_suite.py # 门禁判定 diff --git a/scripts/check_coding_standard.py b/scripts/check_coding_standard.py index 0ee71065..c3eacd1c 100644 --- a/scripts/check_coding_standard.py +++ b/scripts/check_coding_standard.py @@ -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 `、`#include <..\..\x.h>`。 + # ⚠ 2026-10-11 细化:原实现把**任何含 `/` 的 include** 都判违规,于是 + # `` 这类**尖括号系统头**也被误报(模拟器侧两处即中招)。 + # 现按形态区分:尖括号 = 系统/依赖头,允许带子目录(由 -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)) diff --git a/scripts/check_cubemx_survival.py b/scripts/check_cubemx_survival.py index e9a8dd7b..ff39e25c 100644 --- a/scripts/check_cubemx_survival.py +++ b/scripts/check_cubemx_survival.py @@ -142,6 +142,22 @@ CHECKS = [ hint="崩溃现场报告实现(打印 PC/LR/xPSR/CFSR/HFSR/BFAR/MMFAR)" "须在 USER CODE BEGIN 0 区内(生成区内的标准保护位)。", ), + # N-20(2026-10-12):Error_Handler 统一降级。函数体本身在 CubeMX 的 + # `USER CODE BEGIN/END Error_Handler_Debug` 区内(L2 受保护),重生成不丢; + # 本项是 L3 兜底:被打回模板默认(__disable_irq+while(1) 静默挂死)**不报编译错**, + # 只在真出外设初始化故障时暴露——整机只剩 IWDG 兜底且无自检日志。 + dict( + id="error-handler-degrade", + kind="protected", + path="Src/main.c", + region="Error_Handler_Debug", + must_contain=["DBG_ERROR", "NVIC_SystemReset"], + must_not_contain=["__disable_irq", "__builtin_return_address"], + hint="Error_Handler 必须打 1 条自检日志后 NVIC_SystemReset(),不得回到 " + "__disable_irq()+while(1) 静默挂死(无日志、只剩 IWDG 兜底)。" + "⚠ 勿用 __builtin_return_address:AC5 编成库调用,MicroLIB 无符号 = L6218E(2026-10-12 实证)。" + "恢复位置 = `USER CODE BEGIN Error_Handler_Debug` 区。", + ), # ---------------- P1:FreeRTOS 加固 ---------------- # ⚠ 2026-10-09 升级:这两项原先靠 FreeRTOSConfig.h 里**自造**的 # `USER CODE BEGIN FREE_RTOS_CONFIG_HARDENING` 区——模板不认这个区名, @@ -285,6 +301,77 @@ CHECKS = [ # 注:ADC 时钟分频**不设防护**(2026-10-09 决定)。CubeMX 面板里 DIV2 已被约束禁用 # (FadcConstraint Max=36000000;APB2=84 MHz → DIV2=42 MHz 超限,选不到), # .ioc 虽不保存 ADC1.ClockPrescaler 键,重生成也只会回落到约束允许的值。 + # ADC1 上电自校准(2026-10-10 补):RM0090 §13.3.6 的**必需**步骤,CubeMX **不生成**, + # 且本仓库裁剪版 HAL **没有** HAL_ADCEx_Calibration_Start → 只能手写(L2), + # 故必须在此兜底。丢了它不报编译错,只让 ADC 带失调误差(难从日志看出)。 + dict( + id="adc1-calibration-call", + kind="protected", + path="Src/adc.c", + region="ADC1_Init 2", + must_contain=["adc1_calibrate("], + hint="MX_ADC1_Init() 末尾必须调用 adc1_calibrate()(F4 ADC 上电自校准,CubeMX 不生成)。" + "丢了它 = ADC 未校准 → 存在失调误差,且不会报任何编译/运行错误。", + ), + dict( + id="adc1-calibration-impl", + kind="protected", + path="Src/adc.c", + region="1", + must_contain=["int adc1_calibrate(void)", "ADC1_CAL_BIT", "ADC_CR2_ADON"], + hint="自校准实现(清 ADON→等生效→置 ADON→置 CAL→等 CAL 由硬件清零)须在 " + "`USER CODE BEGIN 1` 区。本仓库 HAL 无 HAL_ADCEx_Calibration_Start、" + "CMSIS 头无 ADC_CR2_CAL,两者都是裁剪造成的,勿以为『标准 HAL 里有』。", + ), + # ---------------- ADC 采样节拍 + DMA(2026-10-10 v2.8) ---------------- + # 节拍源 TIM3:同一个更新事件 ① ISR 脉冲 TPAFE CONVST ② 经 TRGO 触发 ADC1 + # ③ 置位 ADC_WAKE_FLAG。丢了 ② 不报编译错,只是两片转换器不再同源(相位差自由漂移)。 + dict( + id="adc-tick-tim3-branch", + kind="protected", + path="Src/main.c", + region="Callback 1", + must_contain=["TIM3", "tpafe5160_start_conv_irq()", "adc_tick_irq_notify()"], + hint="HAL_TIM_PeriodElapsedCallback 的 TIM3 分支必须同时做三件事:判 TIM3、脉冲 CONVST、" + "通知消费者。⚠ 节拍源**不能**换回 TIM6/TIM7——F4 上 ADC1 的外部触发清单里没有 " + "TIM6/TIM7 TRGO(只接 DAC),TIM3 的 TRGO 才是 ADC1 能收到的那个。", + ), + dict( + id="ioc-adc-trig-t3trgo", + kind="ioc", + key="ADC1.ExternalTrigConv", + want="ADC_EXTERNALTRIGCONV_T3_TRGO", + hint="ADC1 外部触发必须是 T3_TRGO(与节拍源 TIM3 配对)。改回软件触发 = 每拍由任务启动" + "转换,DMA 与 TPAFE 不再同源对齐,且重新引入忙等。", + severity="FAIL", + ), + dict( + id="ioc-adc-dma-continuous", + kind="ioc", + key="ADC1.DMAContinuousRequests", + want="ENABLE", + hint="DMAContinuousRequests(DDS 位)必须 ENABLE:关掉后一轮 9 通道只发**一次** DMA 请求" + "→ 静默丢 8 路,不报任何编译/运行错误。", + severity="FAIL", + ), + dict( + id="adc1-dma-link", + kind="generated", + path="Src/adc.c", + must_contain=["hdma_adc1", "__HAL_LINKDMA", "DMA2_Stream4", "DMA_CIRCULAR"], + hint="ADC1 的 DMA 挂载(Stream4 + 循环模式 + __HAL_LINKDMA)须在位。" + "ADC1 在 DMA2 上只挂 Stream0 或 Stream4;选 Stream0 会连锁搬走 SPI1_RX/USART1_RX。", + ), + dict( + id="adc1-dma-irq-priority", + kind="generated", + path="Src/dma.c", + must_contain=["HAL_NVIC_SetPriority(DMA2_Stream4_IRQn, 6, 0)"], + hint="⚠ CubeMX 在**换 DMA 流**时会把该流的 NVIC 优先级静默重置为 **0**(实测两轮)。" + "优先级 0 高于 configMAX_SYSCALL_INTERRUPT_PRIORITY(5) → 会抢占 FreeRTOS 临界区," + "违反本仓『外设中断 ≥5』。每次改 DMA 流后必须回 NVIC 页核对。", + severity="FAIL", + ), ] diff --git a/scripts/check_doc_links.py b/scripts/check_doc_links.py index b2da9d34..5f924e0b 100644 --- a/scripts/check_doc_links.py +++ b/scripts/check_doc_links.py @@ -9,6 +9,9 @@ B. 反引号路径引用 `docs/xxx/yyy.md` —— 相对仓库根解析(本工程约定:文档间引用 一律用「仓库根相对路径」) C. 文件名规范 目录名 / 文件名不得含空格与路径敏感字符(见 docs/文档规范.md §1) + D. 章节引用 `docs/xxx.md` §N.M —— 目标文档里必须真有该编号的标题。 + 刻意只校验**紧邻写法**(文档名与 § 之间只允许空白/反引号/括号, + 实测可把误报压到 0);昵称式引用如「内存方案 §1.4」不校验。 豁免(不算失效,不报错): * 以 `[计划]` 前缀标记的路径(尚未创建的约定产物,见《项目审查与优化建议报告》D-3 建议) @@ -30,6 +33,7 @@ import re import sys import pathlib import urllib.parse +from collections import Counter REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent @@ -61,6 +65,50 @@ STATUS_PROXIMITY = 40 PLACEHOLDER_TOKENS = ("NN_", "XX_", "界面名", "模块名", "日期", "示例", "{{", "}}", "${", "%s", "%d", "<", ">") +# --- D 类:章节引用 §N.M ------------------------------------------------- +# 引用写法:§ + 数字编号(§10 / §10.4 / §2.1.1) +SEC_RE = re.compile(r"§\s*(\d+(?:\.\d+)*)") +# 文档名 token:带目录的路径,或裸文件名(中文/下划线/短横均可)。 +# 非 .md 后缀也要认——`.pre-commit-config.yaml §7` 这类要识别成「非文档锚点」并跳过, +# 否则会误当自引用去校验本文档编号。 +PATH_EXT = r"(?:md|ya?ml|py|c|h|txt|json|ini|ioc|s|bat)" +DOC_NAME_RE = re.compile(r"((?:[\w$.\-一-鿿]+/)+[\w$.\-一-鿿]+\." + PATH_EXT + + r"|[\w$.\-一-鿿]+\." + PATH_EXT + r")") +MD_SUFFIX_RE = re.compile(r"\.md$") +# 裸文档名(不带 .md):紧邻 § 的词,用于「看门狗设计说明 §10」「V0.0 §4.7」这类写法 +# (允许点号,否则 `V0.0` 会被截成 `0`) +STEM_RE = re.compile(r"([A-Za-z0-9_一-鿿][\w.一-鿿]{1,30})[`\s))】\]]{0,4}$") +# 自引用线索:§ 前面是这些词/符号 → 指向本文档 +SELF_CUE_RE = re.compile(r"(见|参见|详见|如|按|第|本文|本节|下文|上文|下面|上方|$" + r"|[((、。::||\[\]])[`\s))】\]]{0,4}$") +# 文档昵称的常见结尾:命中说明 § 前面是「别的文档的名字」,不能当自引用 +DOC_NOUN_TAIL = ("方案", "报告", "规划", "说明", "指南", "流程", "清单", "架构", + "总览", "手册", "规范", "记录", "审计", "分析", "对照", "要求", + "部署", "使用", "设计", "文档", "台账", "总结", "综述") +# 标题与其编号:`## 3. 链接规范` / `### 2.1.1 xxx` +HEAD_RE = re.compile(r"^#{1,6}\s+(.+?)\s*$") +HEAD_NUM_RE = re.compile(r"^(\d+(?:\.\d+)*)\s*[.、))]?\s") +# 文档名与 § 之间允许的最大间隔字符数,且间隔只能是空白/反引号/括号类 +SEC_MAX_GAP = 8 +SEC_GAP_CHARS = "` \t)(()【】[]「」《》、::" +SEC_GAP_RE = re.compile("[" + re.escape(SEC_GAP_CHARS) + "]{0,8}$") +# § 链:同一句内连续引用(`a.md` §3.2 L2 设计点 / §10.2 术语表)——后续 § 继承前一个的目标。 +# 中间允许描述性文字,但不允许句末标点,也不允许再出现文档名(那会换目标) +SEC_CHAIN_MAX = 40 +SEC_TERMINATORS = "。;!?" +# 变更记录类小节:里面的 § 是「当时那个编号」的历史指称,不参与校验 +CHANGELOG_WORDS = ("变更记录", "更新记录", "修订记录", "历史", "changelog") +# 外部资料编号(芯片手册等):不受本仓库标题编号约束 +EXTERNAL_HINTS = ("RM0090", "参考手册", "数据手册", "手册", "datasheet", + "FreeRTOS", "CMSIS", "FatFs", "dhara", "LVGL", "Graphviz", "RFC") +# D 类计数(覆盖率可见化:每次跑都打印,防止比例悄悄回落) +SEC_STATS = Counter() +# 机器判不了目标的 § 引用清单(--section-todo 打印,供逐步改成紧邻写法) +SEC_TODO = [] +# 昵称别名表:昵称 → 仓库根相对路径 +ALIAS_FILE = REPO_ROOT / "scripts" / "section_aliases.json" +_alias_map = None + def is_placeholder(text: str) -> bool: if "*" in text or "<" in text or ">" in text or "{" in text or "}" in text: @@ -149,6 +197,286 @@ def check_backtick_paths(path: pathlib.Path, lines, errors): errors.append((path, lineno, target, "路径引用")) +# --- D 类:章节引用 §N.M ------------------------------------------------- +_doc_index = None # 文件名 -> [Path] +_head_cache = {} # 文档绝对路径 -> 该文档的数字编号标题集合 + + +def build_doc_index(): + """全库 .md 索引(按文件名),用于解析裸文件名引用。""" + global _doc_index + if _doc_index is not None: + return _doc_index + idx = {} + for p in REPO_ROOT.rglob("*.md"): + if is_skipped(p): + continue + idx.setdefault(p.name, []).append(p) + _doc_index = idx + return idx + + +def numbered_headings(path: pathlib.Path): + """目标文档里出现的数字编号标题(`## 3. xxx` / `### 2.1.1 xxx`)。""" + key = str(path) + if key in _head_cache: + return _head_cache[key] + out = set() + try: + text = path.read_text(encoding="utf-8") + except (UnicodeDecodeError, OSError): + _head_cache[key] = out + return out + for line in text.splitlines(): + m = HEAD_RE.match(line) + if not m: + continue + m2 = HEAD_NUM_RE.match(m.group(1).strip()) + if m2: + out.add(m2.group(1)) + _head_cache[key] = out + return out + + +def stem_candidates(token: str, by_stem): + """解析裸文档名:先全名相等,再按前缀(`V0.0` → `V0.0_网表详细分析报告.md`)。 + + 返回候选列表;调用方只在**唯一**时采信,所以「报告」「方案」这类会命中多篇而自动跳过。 + 前缀匹配要求 ≥3 字符,否则「文档」「手册」这类剥剩的通用词会乱认邻居(「主文档」→「文档」 + → 前缀命中「文档规范」)。 + """ + if token in by_stem: + return by_stem[token] + if len(token) < 3: + return [] + out = [] + for stem, paths in by_stem.items(): + if stem.startswith(token): + for p in paths: + if p not in out: + out.append(p) + return out + + +def load_aliases(): + """昵称别名表(`scripts/section_aliases.json`):「内存方案」→ 具体文档。 + + 存在的意义:昵称式引用(「内存方案 §1.4」)读起来顺、机器判不了;登记一次别名, + 正文不用改,引用就纳入校验。表里没有的昵称不猜(宁缺毋滥)。 + """ + global _alias_map + if _alias_map is not None: + return _alias_map + _alias_map = {} + if ALIAS_FILE.exists(): + try: + import json + data = json.loads(ALIAS_FILE.read_text(encoding="utf-8")) + if isinstance(data, dict): + _alias_map = {k: v for k, v in data.items() if isinstance(v, str)} + except (ValueError, OSError, UnicodeDecodeError): + pass + return _alias_map + + +def resolve_name(name: str, by_stem): + """裸文档名 → 目标:先文件名(含前缀唯一),再别名表。判不了返回 (None, None)。""" + cand = stem_candidates(name, by_stem) + if len(cand) == 1: + return cand[0], "stem" + alias = load_aliases().get(name) + apath = (REPO_ROOT / alias) if alias else None + if apath is not None and apath.exists(): + return apath, "alias" + return None, None + + +def suggest_doc(token: str, by_stem): + """给「昵称式引用」猜一个目标文档,用于 --section-todo 的改写建议。 + + 只在**唯一命中**时给建议:昵称整体出现在文件名里,或「昵称前段出现在目录名 + + 昵称后两字出现在文件名」(「内存方案」→ docs/内存管理/RAM使用分析与优化方案.md)。 + """ + if not token or len(token) < 2: + return None + hits = [] + for stem, paths in by_stem.items(): + for p in paths: + rel = _rel(p) + if token in stem or stem in token: + hits.append(p) + elif len(token) > 3 and token[-2:] in stem and token[:-2] in rel: + hits.append(p) + uniq = [] + for p in hits: + if p not in uniq: + uniq.append(p) + return uniq[0] if len(uniq) == 1 else None + + +def sec_plausible(heads, ref: str) -> bool: + """编号存在,或是现有编号的祖先(§8 → 8.2 在位)/后代。""" + return ref in heads or any(h.startswith(ref + ".") for h in heads) + + +def sec_parent_exists(heads, ref: str) -> bool: + """ref 的父级编号在文档里存在——不存在说明这个数量级不属于本文档, + 该 § 其实是「省略了文档名的跨文档引用」,机器判不了(跳过而非误报)。""" + parts = ref.split(".") + if len(parts) == 1: + return any(h.split(".")[0] == parts[0] for h in heads) + parent = ".".join(parts[:-1]) + return parent in heads or any(h.startswith(parent + ".") for h in heads) + + +# 昵称前常挂的动词/介词:「见内存方案 §1.4」「顺序按报告 §6.5」——先剥掉再查别名/文件名 +CUE_STRIP_PREFIXES = ("参见", "详见", "并同步更新", "教训见", "用法与基线见", "反向依赖在", + "改动前先看", "多口需求见", "写入顺序违反", "宏位与", "顺序按", + "依据", "参照", "对照", "违反", "参考", "按", "见", "如", "该", "以", + "与", "和", "及", "或", "并", "在", "的", "第", "看", "主") +# 明确指「本文档」的昵称 → 按自引用处理 +SELF_TOKENS = ("本文档", "本文", "该文", "该文档", "本节", "上文", "下文", "下面", "上方") + + +def strip_cue(token: str) -> str: + prev = None + while prev != token: + prev = token + for c in CUE_STRIP_PREFIXES: + if token.startswith(c) and len(token) > len(c) >= 1: + token = token[len(c):] + break + return token + + +def chain_blocked(between: str, by_stem) -> bool: + """§ 链能否跨过这段文字:出现句末标点、或出现**与某文档文件名完全相同**的裸名就不许跨 + (`… §3 + 看门狗设计说明 §11.4` 里的 §11.4 属于后者,不能继承前一个目标)。 + 只认完全相等:`内容页 / §5.6` 里的「内容页」是描述词,不该断链。""" + if len(between) > SEC_CHAIN_MAX: + return True + if any(t in between for t in SEC_TERMINATORS): + return True + if DOC_NAME_RE.search(between): + return True + for m in re.finditer(r"[A-Za-z0-9_一-鿿][\w.一-鿿]{1,30}", between): + if m.group(0) in by_stem: + return True + return False + + +def check_section_refs(path: pathlib.Path, lines, errors, warns): + """D 类:§N.M 章节引用 —— 目标文档必须真有该编号标题。 + + 目标判定优先级(能判就判,判不了就跳过并计数,口径同 mem-numbers 的「窄而准」): + 1. 紧邻的文档名(`docs/x.md` / `x.md`,≤8 字符间隔)→ 唯一解析即采信 + 2. 同一句内的 § 链(`a.md` §3.2 … / §10.2)→ 继承前一个 § 的目标 + 3. 紧邻的裸文档名(`看门狗设计说明 §10`、`V0.0 §4.7`)→ 全名或前缀唯一匹配 + 4. 自引用线索(见/本文/本节/(/、/句首…)→ 目标 = 本文档 + 跳过:非 .md 锚点(`.pre-commit-config.yaml §7`)、变更记录小节、含状态词的行、 + 外部手册(RM0090 等)、目标文档不用数字编号标题、自引用但父级编号不存在。 + + 两档严重性:**目标明确**的失效 = 错误(拦提交);**自引用**的失效 = 告警 + (打印不拦,因为省略文档名的跨文档引用与真失效机器分不干净)。 + """ + idx = build_doc_index() + by_stem = {} + for name, paths in idx.items(): + for p in paths: + by_stem.setdefault(p.stem, []).append(p) + in_changelog = False + for lineno, line in enumerate(lines, 1): + hm = HEAD_RE.match(line) + if hm: + in_changelog = any(w in hm.group(1) for w in CHANGELOG_WORDS) + if EXAMPLE_MARKER in line or "[计划]" in line: + continue + prev_end = None + prev_tgt = None + prev_kind = None + for m in SEC_RE.finditer(line): + ref = m.group(1) + SEC_STATS["total"] += 1 + window = line[max(0, m.start() - 120):m.start()] + tgt = None + kind = None + names = list(DOC_NAME_RE.finditer(window)) + if names: + last = names[-1] + raw = urllib.parse.unquote(last.group(1)) + # 注意:last 的坐标是**窗口内**的,不能拿 m.start()(整行坐标)去减 + gap_txt = window[last.end():] + near = (len(gap_txt) <= SEC_MAX_GAP and SEC_GAP_RE.match(gap_txt)) + if near and MD_SUFFIX_RE.search(raw): + cand = ([b / raw for b in (path.parent, REPO_ROOT) + if (b / raw).exists()] + if "/" in raw else idx.get(raw, [])) + if len(cand) == 1: + tgt, kind = cand[0], "path" + elif near: + SEC_STATS["skip_nonmd_anchor"] += 1 + prev_end, prev_tgt, prev_kind = None, None, None + continue + if tgt is None and prev_tgt is not None: + between = line[prev_end:m.start()] + if not chain_blocked(between, by_stem): + # 继承前一个 § 的目标**与它的判定档位**(自引用链仍按自引用处理) + tgt, kind = prev_tgt, prev_kind + if tgt is None: + sm = STEM_RE.search(window) + if sm: + stem = sm.group(1) + tgt, kind = resolve_name(stem, by_stem) + if tgt is None: + short = strip_cue(stem) + if stem in SELF_TOKENS or short in SELF_TOKENS: + tgt, kind = path, "self" + elif short != stem: + tgt, kind = resolve_name(short, by_stem) + if tgt is None and short.endswith(DOC_NOUN_TAIL): + SEC_STATS["skip_nickname"] += 1 + SEC_TODO.append((path, lineno, ref, short, "昵称未解析")) + prev_end, prev_tgt, prev_kind = None, None, None + continue + if tgt is None and stem.endswith(DOC_NOUN_TAIL): + SEC_STATS["skip_nickname"] += 1 + SEC_TODO.append((path, lineno, ref, stem, "昵称未解析")) + prev_end, prev_tgt, prev_kind = None, None, None + continue + if tgt is None: + if SELF_CUE_RE.search(window): + tgt, kind = path, "self" + else: + SEC_STATS["skip_unknown"] += 1 + SEC_TODO.append((path, lineno, ref, "", "目标无法判定")) + prev_end, prev_tgt, prev_kind = None, None, None + continue + prev_end, prev_tgt, prev_kind = m.end(), tgt, kind + if any(h in line for h in EXTERNAL_HINTS) or "芯片手册" in window: + SEC_STATS["skip_external"] += 1 + continue + if in_changelog or has_status_hint(line, m.start(), m.end() - m.start()): + SEC_STATS["skip_changelog"] += 1 + continue + heads = numbered_headings(tgt) + if not heads: + SEC_STATS["skip_unnumbered"] += 1 + continue + if sec_plausible(heads, ref): + SEC_STATS["checked"] += 1 + continue + if kind == "self" and not sec_parent_exists(heads, ref): + SEC_STATS["skip_implicit_crossdoc"] += 1 + SEC_TODO.append((path, lineno, ref, "", "省略了文档名")) + continue + SEC_STATS["checked"] += 1 + where = f"§{ref}({tgt.relative_to(REPO_ROOT).as_posix()} 无此编号标题)" + if kind == "self": + warns.append((path, lineno, where)) + else: + errors.append((path, lineno, where, "章节引用")) + + def _rel(p: pathlib.Path) -> str: """仓库相对路径(正斜杠),用于与 SKIP_PARTS 前缀比较。""" try: @@ -210,9 +538,12 @@ def iter_docs(argv): def main(argv): + argv = argv[1:] if len(argv) > 1 else [] + strict = "--sections-strict" in argv errors = [] + warns = [] n_files = 0 - for doc in iter_docs(argv[1:] if len(argv) > 1 else []): + for doc in iter_docs(argv): if is_skipped(doc): continue try: @@ -223,8 +554,55 @@ def main(argv): lines = text.splitlines() check_md_links(doc, lines, errors) check_backtick_paths(doc, lines, errors) + check_section_refs(doc, lines, errors, warns) check_name(doc, errors) + # 覆盖率可见化:每次跑都打印,防止「可校验比例」悄悄回落 + total = SEC_STATS["total"] + checked = SEC_STATS["checked"] + if total: + print(f"章节引用:共 {total} 处,机器可判定 {checked} 处" + f"({100.0 * checked / total:.0f}%);跳过 → 昵称式 {SEC_STATS['skip_nickname']}" + f" / 非文档锚点 {SEC_STATS['skip_nonmd_anchor']}" + f" / 变更记录 {SEC_STATS['skip_changelog']}" + f" / 外部手册 {SEC_STATS['skip_external']}" + f" / 省略文档名的跨文档引用 {SEC_STATS['skip_implicit_crossdoc']}" + f" / 目标无编号体系 {SEC_STATS['skip_unnumbered']}" + f" / 无法判定 {SEC_STATS['skip_unknown']}") + if warns: + print(f"\n⚠️ 章节引用待确认({len(warns)} 处:按自引用解析后本文档没有该编号;" + "不拦提交):") + for path, lineno, where in warns[:40]: + try: + rel = path.relative_to(REPO_ROOT) + except ValueError: + rel = path + print(f" {rel}:{lineno} {where}") + if len(warns) > 40: + print(f" … 其余 {len(warns) - 40} 处略") + print(" 修法:确认它到底指哪篇 → 写成「`路径.md` §N.M」(此后自动纳入硬门禁)," + "或改指本文档现有编号。") + if strict: + for path, lineno, where in warns: + errors.append((path, lineno, where, "章节引用(自引用,--sections-strict)")) + + if "--section-todo" in argv and SEC_TODO: + by_stem = {} + for name, paths in build_doc_index().items(): + for p in paths: + by_stem.setdefault(p.stem, []).append(p) + print(f"\n📋 待改成紧邻写法的章节引用({len(SEC_TODO)} 处,机器判不了目标):") + for path, lineno, ref, token, why in SEC_TODO: + try: + rel = path.relative_to(REPO_ROOT) + except ValueError: + rel = path + sug = suggest_doc(token, by_stem) if token else None + tip = "" + if sug is not None: + tip = f" → 建议改写:`{sug.relative_to(REPO_ROOT).as_posix()}` §{ref}" + print(f" {rel}:{lineno} §{ref} [{why}]{(' ' + token) if token else ''}{tip}") + if errors: print(f"发现的失效文档引用 / 命名问题({len(errors)} 处):") for path, lineno, target, kind in errors: @@ -239,6 +617,9 @@ def main(argv): "已删除的文件请写明「已删除」状态词;" "引用报错样例/日志片段请在该行加 `[示例]` 标记;" "命名规范见 docs/文档规范.md §1。") + print("章节引用失效([章节引用]):目标文档已无该编号小节 → 改指现有编号或直接写小节名。" + "可校验的写法:`路径.md` §N.M / 裸文件名 §N.M / 文档名前缀(V0.0)/ 已登记别名;" + "判不了目标的跑 --section-todo 看清单与改写建议。") return 1 print(f"文档路径检查通过({n_files} 个文档)。") diff --git a/scripts/check_lv_conf_sync.py b/scripts/check_lv_conf_sync.py new file mode 100644 index 00000000..bd2cb19e --- /dev/null +++ b/scripts/check_lv_conf_sync.py @@ -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()) diff --git a/scripts/check_registration.py b/scripts/check_registration.py new file mode 100644 index 00000000..8e300965 --- /dev/null +++ b/scripts/check_registration.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""check_registration.py - 双端注册反向校验门禁(纯标准库)。 + +背景(登记簿 N-23,出处审查报告 N-06 残余): + 本工程 Keil 工程与 PC 模拟器都用**显式文件列表**(无通配扫描)。新 `.c` + 必须手动注册两处才会被编译: + ① `MDK-ARM/STM32F407-Demo.uvprojx` 的 `` 条目(MCU 固件构建) + ② `simulator/CMakeLists.txt` 的显式源列表(PC 模拟器构建) + 历史上 `App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c` **两处都没注册** + = 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效。 + +本脚本做什么(三类缺口): + 扫描 `App/`、`Drivers/BSP/`、`Src/` 下全部 `.c`(排除第三方),对照两份注册表: + ① 存在但未进 uvprojx -> 固件不编译(死代码 / 漏注册) + ② 存在但未进 CMakeLists -> 模拟器不编译(**仅当该文件本应进模拟器时**才是缺口; + MCU-only 文件不进模拟器是**正确**的,见下方豁免) + ③ 注册了但文件不存在(反向)-> 幽灵注册(改名/删除后残留的注册条目) + +豁免机制(关键): + 模拟器只建模 **UI + 网络配置共享核心**;其余模块(MCU 驱动 / 固件任务 / 纯算法 / + CubeMX 生成层)**不进模拟器是设计事实**,模拟器另有 `_sim` 替身或根本无对应。 + 豁免用**显式清单 + 每行理由注释**(命名模式 `*_mcu.c` + 个别点名), + **不做整目录一刀切**(N-23 教训正是"目录级盲区")。唯一例外是 `Src/`: + CubeMX 生成的外设初始化/中断/main,按构造即 MCU 专属、不可能有模拟器替身, + 故整目录豁免(任务口径明确认可"Src/ 全部")。 + +已知缺口过渡(存量不拦、只对新缺口 FAIL): + `KNOWN_GAPS` 登记存量缺口(N-23 存量,Lead 处置中):命中 -> 打印 `[WARN]` 不拦; + 新出现的缺口 -> 打印 `[GAP]` 并 FAIL。`--strict` 关闭白名单(存量也 FAIL),供审计。 + +退出码: + 0 = 一致(无新缺口;存量缺口已白名单 WARN) + 1 = 有缺口(存在未白名单的新缺口或幽灵注册) + 2 = 脚本环境错误(注册表文件缺失 / 无法读取) + +用法(仓库根目录): + & '.venv\\Scripts\\python.exe' scripts/check_registration.py + & '.venv\\Scripts\\python.exe' scripts/check_registration.py --strict + +设计依据:docs/任务规划/登记簿.md N-23;.dsh/skills/new-source-file +创建日期:2026-10-12 +""" + +import argparse +import fnmatch +import os +import re +import sys + +# --- 仓库根(scripts/ 的上一级)--- +REPO_ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +UVPROJX = os.path.join(REPO_ROOT, "MDK-ARM", "STM32F407-Demo.uvprojx") +CMAKELISTS = os.path.join(REPO_ROOT, "simulator", "CMakeLists.txt") +SIM_DIR = os.path.join(REPO_ROOT, "simulator") + +# --- 扫描范围与第三方排除 --- +SCAN_ROOTS = ["App", "Drivers/BSP", "Src"] +# 第三方目录(即便落在扫描根下也跳过) +THIRD_PARTY = [ + "Drivers/STM32F4xx_HAL_Driver", + "Drivers/CMSIS", + "Middlewares", + "Lib", +] + +# --- 模拟器豁免清单(category ②:不进模拟器是**正确**的)--- +# 每项 = (fnmatch 模式(对仓库相对 posix 路径匹配), 理由)。 +# 原则:命名模式 + 个别点名,不整目录一刀切;唯一目录级豁免 Src/(CubeMX 生成,MCU 专属)。 +SIM_EXEMPT = [ + # CubeMX 生成的外设初始化 / 中断向量 / main / freertos:按构造即 MCU 专属, + # 模拟器无对应替身(任务口径明确认可"Src/ 全部")。唯一目录级豁免。 + ("Src/*", "CubeMX 生成的外设初始化/中断/main,MCU 专属,模拟器无替身"), + + # MCU 平台后端:命名约定 *_mcu.c,模拟器另有同名 *_sim.c 替身(在 simulator/sim/)。 + ("*_mcu.c", "MCU 平台后端,模拟器另有 *_sim.c 替身"), + + # --- App/:固件任务 / MCU 依赖 / 纯算法(模拟器无消费路径)--- + ("App/algo/adc_health.c", "L1 有效性校验纯算法,模拟器无消费路径"), + ("App/algo/adc_input.c", "依赖 HAL/TPAFE 取数,MCU 专属"), + ("App/algo/phasor.c", "L2 相量纯算法,模拟器无消费路径"), + ("App/algo/prot_elm.c", "L3 保护元件纯算法,模拟器无消费路径"), + ("App/algo/prot_set.c", "L5 保护定值纯算法,模拟器无消费路径"), + ("App/cfg/prot_set_store.c", "定值持久化 MCU 胶水(依赖 prot_set 纯算法),模拟器无消费路径"), + ("App/algo/sample_ring.c", "L2 环形缓冲纯算法,模拟器无消费路径"), + ("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"), + ("App/fs/vol_lock.c", "FreeRTOS 互斥量卷锁,MCU 专属"), + ("App/log/console_log.c", "MCU 控制台输出,模拟器无消费路径"), + ("App/sync/time_sync.c", "对时任务(net+RTOS),MCU 专属"), + ("App/task/adc_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"), + ("App/task/default_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"), + ("App/task/iec104_serv.c", "固件 FreeRTOS 任务(104 上行,B4),模拟器不跑固件任务"), + ("App/task/protect_task.c", "固件 FreeRTOS 任务(保护判据节拍,B5),模拟器不跑固件任务"), + ("App/task/net_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"), + ("App/task/rs485_task.c", "固件 RS-485 任务,MCU 专属(另:uvprojx 缺口见 KNOWN_GAPS)"), + ("App/task/ui_task.c", "固件 UI 任务,模拟器另有 main.c 驱动 UI"), + ("App/protocol/iec104/iec104_asdu.c", "104 ASDU 编解码纯逻辑,L0.5 主机单测覆盖,模拟器无消费路径"), + ("App/protocol/iec104/iec104_link.c", "104 链路状态机纯逻辑(I/O 注入),L0.5 覆盖,模拟器无消费路径"), + ("App/protocol/iec104/iec104_pts.c", "104 点表/数据源适配(phasor/prot 依赖),模拟器无消费路径"), + ("App/util/sha256.c", "纯工具(FTP 鉴权在 MCU 端),模拟器无消费路径"), + ("App/util/sys_clock.c", "RTC/SD2506 依赖墙钟,MCU 专属"), + ("App/util/task_prof.c", "任务性能探针,MCU 专属"), + ("App/util/task_watchdog.c", "看门狗判定(RTOS 依赖),MCU 专属"), + + # --- Drivers/BSP/:MCU 芯片/网络驱动与 FTP 服务(模拟器无对应)--- + ("Drivers/BSP/CH395F/ch395f.c", "MCU SPI 网卡驱动"), + ("Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c", "MCU SPI NAND 驱动"), + ("Drivers/BSP/GD5F2GQ5UE/nand_ftl.c", "dhara FTL 适配,MCU 存储栈"), + ("Drivers/BSP/NET/lftpd/lftpd.c", "MCU FTP 服务器"), + ("Drivers/BSP/NET/lftpd/lftpd_auth.c", "MCU FTP 鉴权"), + ("Drivers/BSP/NET/lftpd/lftpd_inet.c", "MCU FTP 网络层"), + ("Drivers/BSP/NET/lftpd/lftpd_io.c", "MCU FTP 文件 IO"), + ("Drivers/BSP/NET/lftpd/lftpd_string.c", "MCU FTP 字符串工具"), + ("Drivers/BSP/NET/net_select.c", "MCU socket 选择层"), + ("Drivers/BSP/NET/net_socket.c", "MCU socket 后端,模拟器另有 net_socket_sim.c"), + ("Drivers/BSP/NET/net_listener.c", "MCU 监听器生命周期(依赖 ch395f_* SPI 驱动与 net_socket.c 静态状态),模拟器无对应(Winsock 后端无孤儿态/重监听概念)"), + ("Drivers/BSP/RS485/rs485.c", "MCU RS-485 驱动"), + ("Drivers/BSP/SD2506/sd2506.c", "MCU RTC 驱动"), + ("Drivers/BSP/TPAFE5160/tpafe5160.c", "MCU 计量芯片驱动"), + ("Drivers/BSP/UI/ui_port.c", "MCU UI 端口层(显示/触摸硬件),模拟器另有 sim/sim_port.c"), +] + +# --- 已知缺口白名单(存量,N-23,Lead 已处置)--- +# 命中 -> WARN 不拦;新缺口 -> GAP FAIL。仅针对"存在但未注册"方向。 +KNOWN_GAPS = { + "App/task/rs485_task.c": "E-59 门禁刻意未接线(B2 消费者就绪时随任务立项注册)", +} + + +def to_repo_rel(path): + """把绝对/相对路径归一为仓库相对 posix 路径。""" + p = os.path.normpath(path) + rel = os.path.relpath(p, REPO_ROOT) + return rel.replace("\\", "/") + + +def is_scan_scope(repo_rel): + """是否在扫描范围内(App/、Drivers/BSP/、Src/ 下的 .c,排除第三方)。""" + if not repo_rel.endswith(".c"): + return False + for tp in THIRD_PARTY: + if repo_rel == tp or repo_rel.startswith(tp + "/"): + return False + for root in SCAN_ROOTS: + if repo_rel.startswith(root + "/"): + return True + return False + + +def scan_sources(): + """扫描扫描根下全部 .c,返回仓库相对 posix 路径集合。""" + found = set() + for root in SCAN_ROOTS: + abs_root = os.path.join(REPO_ROOT, root.replace("/", os.sep)) + if not os.path.isdir(abs_root): + continue + for dirpath, _dirnames, filenames in os.walk(abs_root): + for fn in filenames: + if fn.endswith(".c"): + abs_p = os.path.join(dirpath, fn) + rel = to_repo_rel(abs_p) + if is_scan_scope(rel): + found.add(rel) + return found + + +def parse_uvprojx(): + """解析 uvprojx 的 ,返回仓库相对 posix 路径集合(含 .c/.s)。""" + with open(UVPROJX, "r", encoding="utf-8", errors="replace") as f: + text = f.read() + mdk_dir = os.path.dirname(UVPROJX) + entries = set() + for m in re.finditer(r"(.*?)", text): + raw = m.group(1).strip() + if not raw: + continue + # uvprojx 路径相对 MDK-ARM/ 目录,反斜杠或正斜杠混用 + joined = os.path.join(mdk_dir, raw.replace("\\", os.sep)) + entries.add(to_repo_rel(joined)) + return entries + + +def _expand_var(s, varmap, depth=0): + """递归展开 ${VAR}。""" + if depth > 12: + return s + + def repl(m): + name = m.group(1) + val = varmap.get(name, m.group(0)) + return _expand_var(val, varmap, depth + 1) + + return re.sub(r"\$\{(\w+)\}", repl, s) + + +def parse_cmake(): + """解析 CMakeLists 的 *_SOURCES 列表(先展开变量),返回仓库相对 posix 路径集合。""" + with open(CMAKELISTS, "r", encoding="utf-8", errors="replace") as f: + text = f.read() + + # 1) 单值 set(NAME value):目录变量(不含 *_SOURCES) + varmap = {"CMAKE_CURRENT_SOURCE_DIR": SIM_DIR} + for m in re.finditer(r'set\s*\(\s*([A-Za-z_]\w*)\s+("?)([^"\r\n]*?)\2\s*\)', text): + name = m.group(1) + val = m.group(3).strip() + if name.endswith("_SOURCES"): + continue + varmap[name] = val + + # 2) 多值 set(NAME ... ) 中名字以 _SOURCES 结尾的块 + sources = set() + for m in re.finditer(r"set\s*\(\s*([A-Za-z_]\w*_SOURCES)\b(.*?)\)", text, re.DOTALL): + body = m.group(2) + # 逐行剥注释 + lines = [] + for line in body.splitlines(): + idx = line.find("#") + if idx >= 0: + line = line[:idx] + lines.append(line) + body = " ".join(lines) + for tok in body.split(): + expanded = _expand_var(tok, varmap) + if not expanded: + continue + # 展开后若仍是绝对/带盘符路径直接用,否则相对 simulator/ 目录 + if os.path.isabs(expanded) or re.match(r"^[A-Za-z]:[\\/]", expanded): + cand = expanded + else: + cand = os.path.join(SIM_DIR, expanded) + sources.add(to_repo_rel(cand)) + return sources + + +def main(): + ap = argparse.ArgumentParser(description="双端注册反向校验(uvprojx + CMakeLists)") + ap.add_argument("--strict", action="store_true", + help="关闭 KNOWN_GAPS 白名单:存量缺口也按 FAIL 处理(供审计)") + args = ap.parse_args() + + # 环境校验 + for path, label in ((UVPROJX, "uvprojx"), (CMAKELISTS, "CMakeLists.txt")): + if not os.path.isfile(path): + print("[ENV-ERROR] 注册表缺失: %s (%s)" % (label, path)) + return 2 + + try: + scanned = scan_sources() + uv = parse_uvprojx() + cmake = parse_cmake() + except OSError as e: + print("[ENV-ERROR] 读取失败: %s" % e) + return 2 + + uv_c = {p for p in uv if is_scan_scope(p)} + cmake_c = {p for p in cmake if is_scan_scope(p)} + + gaps = [] # 新缺口 -> FAIL + warns = [] # 存量白名单 -> WARN + + # ① 存在但未进 uvprojx(无豁免:扫描范围内每个 .c 都应编入固件) + for f in sorted(scanned): + if f not in uv: + if (not args.strict) and f in KNOWN_GAPS: + warns.append("[WARN] %s | 缺 uvprojx(存量:%s)" % (f, KNOWN_GAPS[f])) + else: + gaps.append("[GAP] %s | 缺 uvprojx" % f) + + # ② 存在但未进 CMakeLists(MCU-only 豁免) + for f in sorted(scanned): + if f in cmake_c: + continue + if any(fnmatch.fnmatch(f, pat) for pat, _ in SIM_EXEMPT): + continue + if (not args.strict) and f in KNOWN_GAPS: + warns.append("[WARN] %s | 缺 CMakeLists(存量:%s)" % (f, KNOWN_GAPS[f])) + else: + gaps.append("[GAP] %s | 缺 CMakeLists" % f) + + # ③ 幽灵注册:注册了但文件不存在(反向) + for f in sorted(uv_c): + if not os.path.isfile(os.path.join(REPO_ROOT, f.replace("/", os.sep))): + gaps.append("[GAP] %s | 幽灵注册(uvprojx)" % f) + for f in sorted(cmake): + if not os.path.isfile(os.path.join(REPO_ROOT, f.replace("/", os.sep))): + gaps.append("[GAP] %s | 幽灵注册(CMakeLists)" % f) + + # 输出 + print("== 双端注册反向校验 ==") + print("扫描 .c: %d | uvprojx(扫描范围): %d | CMakeLists(扫描范围): %d" + % (len(scanned), len(uv_c), len(cmake_c))) + for w in warns: + print(w) + for g in gaps: + print(g) + + if gaps: + print("结果: FAIL(%d 个新缺口/幽灵注册,%d 个存量 WARN)" % (len(gaps), len(warns))) + return 1 + if warns: + print("结果: PASS(无新缺口;%d 个存量缺口已白名单 WARN,N-23 Lead 处置中)" % len(warns)) + return 0 + print("结果: PASS(双端注册一致)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check_test_suite.py b/scripts/check_test_suite.py index 179aa9ec..51078094 100644 --- a/scripts/check_test_suite.py +++ b/scripts/check_test_suite.py @@ -37,7 +37,7 @@ & '.venv\\Scripts\\python.exe' scripts/check_test_suite.py --allow-test-build & '.venv\\Scripts\\python.exe' scripts/check_test_suite.py --file <路径> -设计依据:docs/待处理内容.md T-1;docs/测试说明/测试开发标准流程.md §3.1 +设计依据:docs/任务规划/登记簿.md T-1;docs/测试说明/测试开发标准流程.md §3.1 创建日期:2026-10-01 """ diff --git a/scripts/ci_local.py b/scripts/ci_local.py index aea38aae..a641604d 100644 --- a/scripts/ci_local.py +++ b/scripts/ci_local.py @@ -13,8 +13,8 @@ 刻意**不含**(与 CI 同理由,见 workflow 文件头): - `@build`(需 Keil 授权;CI 也不跑,真编译是开发者本地红线) - - `mem-numbers` 钩子(事实源 `.map` 是 Keil 产物——**本地若存在 .map 就照常跑**, - 与 CI 的自适应逻辑一致) + - `mem-numbers` / `mem-budget` 钩子(事实源 `.map` 是 Keil 产物——**本地若存在 .map + 就照常跑**,与 CI 的自适应逻辑一致) 退出码: 0 = 全部通过(或不适用项已按规则跳过) @@ -147,17 +147,20 @@ def run_l0(): print("=" * 64) env = dict(os.environ) - # 与 CI 相同的自适应:无 .map(未做 Keil 构建)=> mem-numbers 不适用 => 跳过 + # 与 CI 相同的自适应:无 .map(未做 Keil 构建)=> mem-numbers / mem-budget + # 不适用 => 跳过(两者事实源都是 Keil 产物 .map) map_path = _find_map() if map_path: print("[INFO] 找到 .map:{}".format(map_path.relative_to(REPO_ROOT))) - print("[INFO] mem-numbers 照常运行") + print("[INFO] mem-numbers / mem-budget 照常运行") else: - print("[INFO] 未找到 .map(未做 Keil 构建)-> 跳过 mem-numbers(与 CI 一致)") + print("[INFO] 未找到 .map(未做 Keil 构建)-> 跳过 mem-numbers / " + "mem-budget(与 CI 一致)") print("[INFO] :warning: 体积门禁未执行——改代码后请本地 @build + " - "check_mem_numbers.py --scan 兜底") + "check_mem_numbers.py --scan + ram_baseline.py --gate 兜底") skip = env.get("SKIP", "") - env["SKIP"] = (skip + "," if skip else "") + "mem-numbers" + add = "mem-numbers,mem-budget" + env["SKIP"] = (skip + "," if skip else "") + add rc, text = _run_capture([PY, "-m", "pre_commit", "run", "--all-files"], env=env) # 汇总行(对齐 CI 的 ::notice::):统计钩子 Passed/Failed/Skipped diff --git a/scripts/gen_task_table.py b/scripts/gen_task_table.py index 280de591..e874fdce 100644 --- a/scripts/gen_task_table.py +++ b/scripts/gen_task_table.py @@ -75,7 +75,7 @@ ROLE = { "testTask": "硬件测试入口", "uiTask": "LVGL UI 界面", "timeSyncTask": "墙钟时间同步", - "adcTask": "TPAFE5160 采样(1ms 节拍 + 原子快照)", + "adcTask": "TPAFE5160 采样(TIM3 1kHz 事件驱动 + 原子快照 + STM32 ADC DMA)", } diff --git a/scripts/ram_baseline.py b/scripts/ram_baseline.py new file mode 100644 index 00000000..36d3cee1 --- /dev/null +++ b/scripts/ram_baseline.py @@ -0,0 +1,407 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +RAM 预算门禁 + 趋势台账(登记簿 N-27 前半 / 审查报告 §5.5 三件套,2026-10-12)。 + +三件套的机械部分收在本脚本(事实源 = Keil `.map`,与 `check_mem_numbers.py` 同口径): + + 1. **92% 阈值门禁**(`--gate`,pre-commit `mem-budget` 钩子入口): + RW_IRAM1 / RW_CCM 任一区占比 > 92% → FAIL,并附 Top-10 RW+ZI 对象表; + 无 `.map`(未做 Keil 构建)→ 自适应跳过(与 `mem-numbers` 同口径, + CI 盲区登记在 `docs/任务规划/测试与架构专项.md` A-5 残余)。 + `--gate` 同时校验台账受管区与 `.map` 一致(防漂移,同 `mem-numbers` 思路)。 + 2. **趋势基线** `scripts/ram_baseline.tsv`(`--update` 每次 @build 后追加一行; + `--trend` 看趋势并高亮 RW+ZI 较上一行增长 >1 KB 的对象——方案 §0.3 + "ZI 增量 >1KB 必须解释"的机械化)。 + 3. **台账受管区**(`--write` 同步 `docs/内存管理/RAM预算台账.md` 的 + `MEM_BUDGET:BEGIN/END` 区:区域水位 + 子系统配额对照)。 + +子系统配额(报告 §5.5 第 3 条的规划值;RTOS 堆 2026-10-12 B5 48→56 KB): + 网络 12 KB / 存储 12 KB / UI 16 KB / RTOS 堆 56 KB / 应用 8 KB + ——UI 配额是 R-9/R-2 落地后的**目标态**,现状超配 = W3 债务,台账如实展示; + 配额改动 = 先登记 `docs/内存管理/RAM预算台账.md` 变更记录,再改本脚本 `QUOTAS`。 + +用法(仓库根): + python scripts/ram_baseline.py --gate + python scripts/ram_baseline.py --write + python scripts/ram_baseline.py --update + python scripts/ram_baseline.py --trend [-n 10] + +退出码:0 通过/跳过;1 门禁失败或受管区漂移;2 事实源解析失败。 +创建日期:2026-10-12(W1-0,登记簿 N-27 前半) +""" +import argparse +import pathlib +import sys +from datetime import datetime + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) +import ram_report # noqa: E402 同目录:复用 .map 解析(regions + components,口径一致) +from check_mem_numbers import read_doc, write_doc # noqa: E402 保真读写(BOM/CRLF) + +REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent +MAP_DEFAULT = REPO_ROOT / "MDK-ARM" / "STM32F407-Demo" / "STM32F407-Demo.map" +TSV_PATH = REPO_ROOT / "scripts" / "ram_baseline.tsv" +LEDGER = REPO_ROOT / "docs" / "内存管理" / "RAM预算台账.md" + +BEGIN = "" +END = "" + +THRESHOLD_PCT = 92.0 # 报告 §5.5 第 1 条 +GATED_REGIONS = ("RW_IRAM1", "RW_CCM") # 门禁约束区;其余区仅展示 +GROWTH_ALERT_B = 1024 # 方案 §0.3:增量 >1 KB 必须解释 +TOP_N = 10 # tsv 记录 / 失败附表的对象数 + +RTOS_HEAP = "RTOS 堆" +SUBSYSTEMS = ("网络", "存储", "UI", RTOS_HEAP, "应用", "其它") +QUOTAS = {"网络": 12 * 1024, "存储": 12 * 1024, "UI": 16 * 1024, + RTOS_HEAP: 56 * 1024, "应用": 8 * 1024} # RTOS 堆 48→56 KB:2026-10-12 B5(台账 §6) + +# dhara 是裸对象名(nand.c/map.c/...),必须精确匹配,勿用前缀 +_DHARA = {"map.o", "journal.o", "nand.o", "fctl.o", "era.o"} +# RTOS 内核静态对象:归"其它"(RTOS 配额只约束 CCM 堆本体,见台账 §2 口径表) +_KERNEL = {"tasks.o", "queue.o", "timers.o", "event_groups.o", + "list.o", "port.o", "cmsis_os2.o", "freertos.o"} + + +def subsystem_of(obj): + """对象名 → 子系统(前缀首匹配;口径表见 RAM预算台账.md §2)。""" + n = obj.strip().lower() + if n.startswith(("lv_", "ui_")): + return "UI" + if n.startswith(("ch395f", "net_", "lftpd", "ftp_task")): + return "网络" + if (n.startswith(("gd5f", "nand_", "ff", "diskio", "vol_lock")) + or n in _DHARA): + return "存储" + if n == "heap_4.o": + return RTOS_HEAP + if n in _KERNEL: + return "其它" + if n.startswith(("app_", "adc_", "prot_", "phasor", "sample_ring", + "sys_", "cfg_", "dbg_", "console_log", "task_watchdog", + "sha256", "tpafe", "sd2506", "test_task", "time_sync", + "default_task")): + return "应用" + return "其它" + + +def collect(map_path): + """解析 .map → {'regions': {name: r}, 'subsys': {name: bytes}, 'top': [(obj, B)]}。""" + data = ram_report.parse_map(str(map_path)) + regions = {r["name"]: r for r in data["regions"]} + comps = [c for c in data["components"] + if not ram_report._is_aggregate(c["object"]) + and not c["object"].startswith("(incl.")] + subsys = {s: 0 for s in SUBSYSTEMS} + for c in comps: + subsys[subsystem_of(c["object"])] += c["rw_data"] + c["zi_data"] + top = sorted(((c["object"], c["rw_data"] + c["zi_data"]) for c in comps), + key=lambda t: (-t[1], t[0])) + top = [t for t in top if t[1] > 0][:TOP_N] + return {"regions": regions, "region_list": data["regions"], + "subsys": subsys, "top": top} + + +def _util(r): + return r["size"] / r["max"] * 100 if r["max"] else 0.0 + + +def _quota_status(name, used): + if name not in QUOTAS: + return "参考" + if name == RTOS_HEAP and used - QUOTAS[name] <= 64: + # heap_4.o = 堆本体 49,152 B(= 配额)+ 32 B RW 元数据,不算超 + return "堆=配额" + if used <= QUOTAS[name]: + return "OK" + return f"⚠ 超 {(used - QUOTAS[name]) / 1024:.1f} KB" + + +def render_budget(info): + """渲染台账受管区(按 .map 区出现顺序动态渲染,新增区自动入表; + 门禁列只约束 GATED_REGIONS,其余区标"参考")。""" + L = [] + L.append("| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |") + L.append("|---|---|---|---|---|") + for r in info["region_list"]: + gated = r["name"] in GATED_REGIONS + label = f"{r['name']} (Flash)" if r["name"] == "ER_IROM1" else r["name"] + u = _util(r) + mark = ("PASS" if u <= THRESHOLD_PCT else "FAIL") if gated else "参考" + L.append(f"| {label} | {r['size']:,} B | {r['max']:,} B | " + f"{u:.1f}% | {mark} |") + L.append("") + L.append("| 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 |") + L.append("|---|---|---|---|") + for s in SUBSYSTEMS: + used = info["subsys"][s] + quota = f"{QUOTAS[s] // 1024} KB" if s in QUOTAS else "—" + L.append(f"| {s} | {used:,} B | {quota} | {_quota_status(s, used)} |") + return "\n".join(L) + + +# --- 受管区读写(锚点语义同 check_mem_numbers.py) --------------------------- + +def extract_managed(text): + if BEGIN not in text or END not in text: + return None + s = text.index(BEGIN) + len(BEGIN) + e = text.index(END) + return text[s:e].strip() + + +def replace_managed(text, block): + s = text.index(BEGIN) + len(BEGIN) + e = text.index(END) + new = "\n" + block + "\n" + if text[s:e] == new: + return text, False + return text[:s] + new + text[e:], True + + +# --- 子命令 ------------------------------------------------------------------ + +def _map_or_skip(args): + map_path = pathlib.Path(args.map) + if not map_path.exists(): + return None + return map_path + + +def cmd_gate(args): + map_path = _map_or_skip(args) + if map_path is None: + print("[SKIP] 无 .map(未做 Keil 构建)→ RAM 预算门禁不适用") + print(" (CI 盲区已登记 docs/任务规划/测试与架构专项.md A-5 残余;" + "本地改过代码请先 @build)") + return 0 + info = collect(map_path) + if not info["regions"]: + print(f"[FAIL] 未能从 {map_path.name} 解析出执行区", file=sys.stderr) + return 2 + + fails = [] + for name in ("RW_IRAM1", "RW_CCM"): + r = info["regions"].get(name) + if not r: + fails.append(f"缺执行区 {name}(scatter 变更?)") + continue + u = _util(r) + if u > THRESHOLD_PCT: + fails.append(f"{name} 占用 {u:.1f}% > {THRESHOLD_PCT:.0f}%" + f"({r['size']:,}/{r['max']:,} B)") + if fails: + print("[FAIL] RAM 预算超阈值:", file=sys.stderr) + for f in fails: + print(f" - {f}", file=sys.stderr) + print(f"\nTop-{TOP_N} 静态对象(RW+ZI,找增量元凶):", file=sys.stderr) + for obj, b in info["top"]: + print(f" {b:>7,} B {obj}", file=sys.stderr) + print("\n处置:削峰(静态化回退/缓冲复用)或按 docs/内存管理/RAM预算台账.md" + " §5 走超配额流程;勿盲目抬 scatter。", file=sys.stderr) + return 1 + + # 台账受管区一致性(防漂移;同 mem-numbers 的"事实源唯一"思路) + if not LEDGER.exists(): + print(f"[FAIL] 台账缺失:{LEDGER.relative_to(REPO_ROOT)}", file=sys.stderr) + return 1 + text, newline, bom = read_doc(LEDGER) + expected = render_budget(info) + got = extract_managed(text) + if got is None: + print(f"[FAIL] 台账缺受管锚点:{LEDGER.relative_to(REPO_ROOT)}", + file=sys.stderr) + return 1 + if got != expected: + print("[FAIL] 台账受管区与 .map 不一致(区域水位或子系统配额表漂移)", + file=sys.stderr) + print(f"事实源:{map_path.relative_to(REPO_ROOT)}", file=sys.stderr) + print("修复:python scripts/ram_baseline.py --write", file=sys.stderr) + return 1 + + # 软提醒(不拦):tsv 落后 / .map 比源码旧 + if TSV_PATH.exists(): + rows = _read_tsv() + ir = info["regions"].get("RW_IRAM1") + if rows and ir and int(rows[-1]["iram1"]) != ir["size"]: + print("[WARN] ram_baseline.tsv 末行落后于当前 .map —— " + "@build 后请跑 `python scripts/ram_baseline.py --update`") + else: + print("[WARN] ram_baseline.tsv 不存在 —— 跑 " + "`python scripts/ram_baseline.py --update` 建档") + newest = _newest_source_mtime() + if newest and newest > map_path.stat().st_mtime: + print("[WARN] .map 早于最新源码——数字来自旧构建,建议先 @build 再提交") + + ir, cc = info["regions"].get("RW_IRAM1"), info["regions"].get("RW_CCM") + print(f"RAM 预算门禁 PASS:RW_IRAM1 {_util(ir):.1f}% / RW_CCM {_util(cc):.1f}%" + f"(阈值 {THRESHOLD_PCT:.0f}%)") + return 0 + + +def cmd_write(args): + map_path = _map_or_skip(args) + if map_path is None: + print("[FAIL] 找不到 .map 事实源,先 @build", file=sys.stderr) + return 1 + info = collect(map_path) + if not LEDGER.exists(): + print(f"[FAIL] 台账不存在:{LEDGER.relative_to(REPO_ROOT)}", file=sys.stderr) + return 1 + text, newline, bom = read_doc(LEDGER) + if BEGIN not in text or END not in text: + print("[FAIL] 台账缺受管锚点", file=sys.stderr) + return 1 + new_text, did = replace_managed(text, render_budget(info)) + if did: + write_doc(LEDGER, new_text, newline, bom) + print("已同步台账受管区。") + else: + print("台账受管区已是最新。") + return 0 + + +# --- tsv 趋势基线 ------------------------------------------------------------ + +TSV_HEADER = ("date\tcode\tro\trw\tzi\ter_irom1\ter_pct\tiram1\tiram1_pct" + "\tccm\tccm_pct\ttop_rwzi") + + +def _read_tsv(): + rows = [] + lines = TSV_PATH.read_text(encoding="utf-8").splitlines() + if len(lines) < 2: + return rows + keys = lines[0].split("\t") + for ln in lines[1:]: + parts = ln.split("\t") + if len(parts) == len(keys): + rows.append(dict(zip(keys, parts))) + return rows + + +def _top_field(top): + return ";".join(f"{obj}={b}" for obj, b in top) + + +def cmd_update(args): + map_path = _map_or_skip(args) + if map_path is None: + print("[FAIL] 找不到 .map 事实源,先 @build", file=sys.stderr) + return 1 + info = collect(map_path) + gt = ram_report.parse_map(str(map_path))["grand_totals"] + if not gt: + print("[FAIL] .map 缺 Grand Totals", file=sys.stderr) + return 2 + today = datetime.now().strftime("%Y-%m-%d") + reg = info["regions"] + row = [ + today, str(gt["code"]), str(gt["ro_data"]), str(gt["rw_data"]), + str(gt["zi_data"]), + str(reg["ER_IROM1"]["size"]), f"{_util(reg['ER_IROM1']):.1f}", + str(reg["RW_IRAM1"]["size"]), f"{_util(reg['RW_IRAM1']):.1f}", + str(reg["RW_CCM"]["size"]), f"{_util(reg['RW_CCM']):.1f}", + _top_field(info["top"]), + ] + lines = (TSV_PATH.read_text(encoding="utf-8").splitlines() + if TSV_PATH.exists() else []) + if not lines: + lines = TSV_HEADER.split("\t") + lines = [TSV_HEADER] + body = [ln for ln in lines[1:] if ln.strip() + and not ln.split("\t")[0] == today] + body.append("\t".join(row)) + # newline="\n":Windows 文本模式默认 \n→\r\n,违反 .gitattributes eol=lf + TSV_PATH.write_text("\n".join([lines[0]] + body) + "\n", + encoding="utf-8", newline="\n") + print(f"已记录 {today}:RW_IRAM1 {reg['RW_IRAM1']['size']:,} B " + f"({_util(reg['RW_IRAM1']):.1f}%) / RW_CCM {reg['RW_CCM']['size']:,} B " + f"({_util(reg['RW_CCM']):.1f}%) → {TSV_PATH.relative_to(REPO_ROOT)}") + return 0 + + +def cmd_trend(args): + if not TSV_PATH.exists(): + print("[WARN] tsv 不存在——先跑 --update 建档") + return 0 + rows = _read_tsv() + if not rows: + print("[WARN] tsv 为空") + return 0 + print(f"趋势(最近 {min(args.n, len(rows))} 行,共 {len(rows)} 行):") + print(f"{'日期':<12} {'RW_IRAM1':>12} {'RW_CCM':>12} {'Flash':>12}") + for r in rows[-args.n:]: + print(f"{r['date']:<12} {int(r['iram1']):>9,} B ({r['iram1_pct']:>4}%)" + f" {int(r['ccm']):>7,} B ({r['ccm_pct']:>4}%)" + f" {int(r['er_irom1']):>7,} B ({r['er_pct']:>4}%)") + if len(rows) >= 2: + prev = dict() + for kv in rows[-2]["top_rwzi"].split(";"): + if "=" in kv: + obj, b = kv.rsplit("=", 1) + prev[obj] = int(b) + grew = [] + for kv in rows[-1]["top_rwzi"].split(";"): + if "=" not in kv: + continue + obj, b = kv.rsplit("=", 1) + delta = int(b) - prev.get(obj, 0) + if delta > GROWTH_ALERT_B: + grew.append((obj, prev.get(obj, 0), int(b), delta)) + if grew: + print(f"\n⚠ 较上一行 RW+ZI 增长 >{GROWTH_ALERT_B} B 的对象" + f"(提交说明须解释,方案 §0.3):") + for obj, old, new, d in sorted(grew, key=lambda t: -t[3]): + print(f" +{d:>6,} B {obj} ({old:,} → {new:,})") + else: + print("\nTop 对象无 >1 KB 的异常增长。") + return 0 + + +# --- 工具 -------------------------------------------------------------------- + +def _newest_source_mtime(): + newest = 0 + for base in ("App", "Drivers/BSP", "Src", "Inc"): + d = REPO_ROOT / base + if not d.is_dir(): + continue + for p in d.rglob("*"): + if p.suffix in (".c", ".h") and p.is_file(): + newest = max(newest, p.stat().st_mtime) + lv = REPO_ROOT / "Lib" / "lv_conf.h" + if lv.is_file(): + newest = max(newest, lv.stat().st_mtime) + return newest + + +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + try: + stream.reconfigure(encoding="utf-8", errors="replace") + except Exception: + pass + ap = argparse.ArgumentParser( + description="RAM 预算门禁 + 趋势台账(N-27 前半 / 报告 §5.5 三件套)") + g = ap.add_mutually_exclusive_group() + g.add_argument("--gate", action="store_true", help="92% 阈值门禁(pre-commit 入口)") + g.add_argument("--write", action="store_true", help="同步台账受管区") + g.add_argument("--update", action="store_true", help="追加 tsv 趋势行(@build 后跑)") + g.add_argument("--trend", action="store_true", help="打印趋势 + 高亮 >1 KB 增长") + ap.add_argument("-n", type=int, default=10, help="--trend 显示行数") + ap.add_argument("--map", default=str(MAP_DEFAULT), help=".map 路径") + args = ap.parse_args(argv) + + if args.write: + return cmd_write(args) + if args.update: + return cmd_update(args) + if args.trend: + return cmd_trend(args) + return cmd_gate(args) # 默认 = --gate + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv new file mode 100644 index 00000000..3a6276e7 --- /dev/null +++ b/scripts/ram_baseline.tsv @@ -0,0 +1,2 @@ +date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi +2026-10-12 205516 18044 1036 152484 223560 21.3 96096 73.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;console_log.o=4120;ch395f.o=4106;default_task.o=2736;sys_log_mcu.o=1924 diff --git a/scripts/ram_report.py b/scripts/ram_report.py index 5634c337..73dde519 100644 --- a/scripts/ram_report.py +++ b/scripts/ram_report.py @@ -19,17 +19,23 @@ MAP_DEFAULT = os.path.join( ) # Rows in "Image component sizes" that re-aggregate objects (would double-count sums). -TOTALS_ROWS = {"Object Totals", "Library Totals", "User Totals", "mc_w.l", "mf_w.l"} +TOTALS_ROWS = {"Object Totals", "Library Totals", "User Totals"} + +# ARM library aggregate rows (e.g. mc_w.l / mf_w.l / m_wm.l): the linker prints one +# aggregate line per library in addition to the individual member .o lines, so any +# ".l" row is a re-aggregation and must be dropped from per-file sums. +# (2026-10-11: sha256's 64-bit shifts pulled in m_wm.l — the hardcoded name list +# missed it and the self-consistency check went +58; match the name pattern instead.) +_LIB_AGG_RE = re.compile(r'^[a-z]{1,2}_[a-z0-9]+\.l$') def _is_aggregate(name): """Aggregate rows to drop from per-file sums: library-name summaries and Totals lines. - '(incl. Padding)' / '(incl. Generated)' are linker metadata; they are excluded from - per-file tables but their contribution IS counted in Grand Totals, so the per-file - sums will differ from Grand Totals by the alignment-padding delta (~300B here). + '(incl. Padding)' / '(incl. Generated)' are linker metadata ledger rows; they ARE + kept so that per-file sums reconcile exactly with Grand Totals (self-check Δ=0). """ - return name in TOTALS_ROWS + return name in TOTALS_ROWS or bool(_LIB_AGG_RE.match(name)) def parse_execution_region_header(line): diff --git a/scripts/rename_ref.py b/scripts/rename_ref.py new file mode 100644 index 00000000..947ba7a5 --- /dev/null +++ b/scripts/rename_ref.py @@ -0,0 +1,395 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +改名 / 挪目录 + 全库引用自动改写(把"同步引用链接"从手工活变成一条命令)。 + +背景:本仓库 1800+ 处路径引用(`docs/xxx.md` 反引号引用 + Markdown 链接 + +`#include` + `uvprojx`/`CMakeLists.txt` 源文件列表),改名或挪目录后要靠人 +逐文件扫改(历史上一次改名波及 10~30 个文件)。本脚本把这件事机械化。 + +用法: + python scripts/rename_ref.py <旧路径> <新路径> # 默认 dry-run,只打印将要改什么 + python scripts/rename_ref.py <旧路径> <新路径> --apply # 真正落盘(git mv + 改写引用) + python scripts/rename_ref.py <旧路径> <新目录/ # 挪进目录(保留文件名) + python scripts/rename_ref.py docs/a/ docs/b/ --apply # 整目录搬迁(前缀改写) + +路径一律写**仓库根相对路径**(正斜杠),末尾不带 `/` 也可。 + +改写覆盖的引用形态: + 1. 反引号根相对引用 `docs/xxx/yyy.md` + 2. Markdown 链接 [文本](docs/xxx/yyy.md)(根相对) / [文本](../xxx/yyy.md)(文档相对) + 3. 图片链接 ![x](images/a.svg) —— 含"文档挪层级后 ../ 前缀要跟着变"这条实证坑 + 4. C 的 #include #include "yyy.h"(改名时连裸文件名引用一起改) + 5. 构建文件 MDK-ARM/*.uvprojx 的 、simulator/CMakeLists.txt 的 + ${PROJECT_ROOT}/… 与 ${VAR}/… 源文件列表 + 6. 任意文本文件里的路径串(.py/.bat/.yaml/.json/.txt/skill 等) + +保真约定(见 docs/文档规范.md §2、scripts/README.md「脚本编写约定」): + * 按原样回写 BOM 与换行符(LF/CRLF),无改动的文件不写 + * 只扫 git 跟踪的文本文件;第三方/生成物目录跳过 + * 默认 dry-run;--apply 才落盘,且落盘后自动跑 check_doc_links.py --all 复核 + +退出码:0 成功;1 有错误(旧路径不存在 / 新路径已存在 / 复核失败);2 用法错误。 +""" +import argparse +import os +import pathlib +import re +import subprocess +import sys +import urllib.parse + +REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent + +# 不扫描的路径片段(第三方 / 生成物 / 本地状态) +SKIP_PARTS = ("Lib/", "Middlewares/", "Drivers/STM32F4xx_HAL_Driver/", + "Drivers/CMSIS/", "MDK-ARM/objects/", "MDK-ARM/Listings/", + "MDK-ARM/RTE/", "MDK-ARM/STM32F407-Demo/", ".venv/", + "node_modules/", "__pycache__/", "build/", "obj/", + ".workbuddy-ai/", ".mnemon/", "simulator/SDL2/", + "docs/芯片手册/") +# 生成物:不改写,只在结尾提示重跑生成器(改了也是白改,下次生成会覆盖) +GENERATED = ("docs/images/引用关系图.",) + +# 不扫描的扩展名(二进制 / 生成物) +SKIP_EXT = {".o", ".d", ".map", ".axf", ".bin", ".hex", ".elf", ".htm", ".lst", + ".png", ".jpg", ".jpeg", ".gif", ".ttf", ".woff", ".dll", ".lib", + ".pyc", ".zip", ".gz", ".pdf", ".docx", ".xlsx", ".ico", ".cur"} + +# 路径 token:至少含一个分隔符;允许 ${VAR} 前缀、中文、点、下划线、短横 +CH = r"[\w$.\-{}一-鿿]" +TOKEN_RE = re.compile(CH + r"+(?:[/\\]" + CH + r"+)+") +VAR_PREFIX_RE = re.compile(r"^(\$\{[^}]+\}/)(.+)$") +INCLUDE_RE = re.compile(r'(#\s*include\s*)([<"])([^<>"\n]+)([">])') +FILEPATH_RE = re.compile(r"()([^<\n]+)()") + +BOM = b"\xef\xbb\xbf" + + +def rel(p: pathlib.Path) -> str: + """仓库根相对 POSIX 路径。""" + try: + return p.resolve().relative_to(REPO_ROOT).as_posix() + except ValueError: + return p.as_posix() + + +def is_skipped(relpath: str) -> bool: + for s in SKIP_PARTS: + if relpath == s.rstrip("/") or relpath.startswith(s): + return True + return False + + +def is_generated(relpath: str) -> bool: + """生成物(引用关系图等):跳过改写,只提示重跑生成器。""" + return any(relpath.startswith(g) for g in GENERATED) + + +def tracked_files(): + """git 跟踪 + 未跟踪但未忽略的文件(自动排除生成物与 .gitignore 内容)。""" + out = subprocess.run(["git", "ls-files", "-z", "--cached", "--others", + "--exclude-standard"], + cwd=REPO_ROOT, stdout=subprocess.PIPE, check=True).stdout + for name in out.decode("utf-8").split("\0"): + if not name: + continue + name = name.replace("\\", "/") + if is_skipped(name): + continue + if pathlib.Path(name).suffix.lower() in SKIP_EXT: + continue + p = REPO_ROOT / name + if p.is_file(): + yield p + + +def read_text(p: pathlib.Path): + """返回 (text, bom, ok)。二进制/非 UTF-8 → ok=False。""" + data = p.read_bytes() + bom = data.startswith(BOM) + if bom: + data = data[len(BOM):] + try: + return data.decode("utf-8"), bom, True + except UnicodeDecodeError: + return None, bom, False + + +def resolve_token(tok: str, file_dir: pathlib.Path, old_abs: pathlib.Path): + """把 token 解析成绝对路径;解析不到 old 就返回 (None, "", False)。 + + 返回 (解析到的绝对路径, ${VAR}/ 前缀, 是否按仓库根解析)。 + 依次尝试:${VAR}/ 前缀剥离 → 相对仓库根 → 相对引用文件目录 → 尾段后缀匹配。 + """ + var_prefix = "" + body = tok + m = VAR_PREFIX_RE.match(tok) + if m: + var_prefix, body = m.group(1), m.group(2) + + unquoted = urllib.parse.unquote(body) + roots = [] + if not unquoted.startswith("."): + roots.append((REPO_ROOT, True)) + roots.append((file_dir, False)) + if unquoted.startswith("."): + roots.insert(0, (REPO_ROOT, True)) + for base, via_root in roots: + for b in (unquoted, body): + b2 = b.rstrip(".") + try: + rc = pathlib.Path(os.path.normpath(base / b2)) + except (OSError, ValueError): + continue + if rc == old_abs or str(rc).startswith(str(old_abs) + os.sep): + return rc, var_prefix, via_root + + # ${VAR}/name.ext 之类:变量指向的目录未知,按「尾段后缀」匹配 + parts = re.split(r"[/\\]", body) + old_parts = list(old_abs.parts) + if var_prefix and len(parts) <= 2: + if parts[-1] == old_parts[-1] and (len(parts) == 1 or parts[-2] == old_parts[-2]): + return old_abs, var_prefix, True + return None, "", False + + +def resolve_relative(tok: str, file_dir: pathlib.Path): + """按「相对引用文件所在目录」解析,且目标必须真实存在(用于被移动文件的出链)。""" + if VAR_PREFIX_RE.match(tok): + return None + for b in (urllib.parse.unquote(tok), tok): + b2 = b.rstrip(".") + p = pathlib.Path(os.path.normpath(file_dir / b2)) + if p.exists(): + return p + return None + + +def rel_spelling(tok: str, target: pathlib.Path, base_dir: pathlib.Path) -> str: + """保持原分隔符风格,重算相对 base_dir 的写法。""" + body = os.path.relpath(target, base_dir).replace("\\", "/") + if "\\" in tok: + body = body.replace("/", "\\") + if "%" in tok: + body = urllib.parse.quote(body, safe="/") + return body + + +def new_spelling(tok: str, resolved: pathlib.Path, var_prefix: str, via_root: bool, + base_dir: pathlib.Path) -> str: + """按原写法风格算出新 token(根相对保持根相对,相对保持相对)。""" + old_abs = old_abs_global[0] + new_abs = new_abs_global[0] + if resolved == old_abs: + remainder = pathlib.Path(".") + else: + remainder = resolved.relative_to(old_abs) + target = new_abs if remainder == pathlib.Path(".") else new_abs / remainder + + if var_prefix or via_root: + new_body = os.path.relpath(target, REPO_ROOT) + else: + new_body = os.path.relpath(target, base_dir) + if "\\" in tok: + new_body = new_body.replace("/", "\\") + else: + new_body = new_body.replace("\\", "/") + if "%" in tok: + new_body = urllib.parse.quote(new_body, safe="/") + return var_prefix + new_body + + +old_abs_global = [None] +new_abs_global = [None] + + +def rewrite_text(text: str, file_path: pathlib.Path, changes: list, lineno_map=True): + """改写一个文件里的所有引用;changes 收集 (lineno, old, new)。""" + old_abs = old_abs_global[0] + new_abs = new_abs_global[0] + file_abs = file_path.resolve() + # 被移动文件(含整目录搬迁时目录内的文件)自身的相对链接(图片等): + # **解析**用旧目录(链接当前指向的位置),**改写**用新目录 + # ——docs/文档规范.md §3.2 记的实证坑(迁目录后 images/ 未跟着改 ../) + moved = (file_abs == old_abs + or str(file_abs).startswith(str(old_abs) + os.sep)) + resolve_dir = file_path.parent + emit_dir = new_abs.parent if moved else file_path.parent + + out = [] + pos = 0 + for m in TOKEN_RE.finditer(text): + tok = m.group(0) + resolved, var_prefix, via_root = resolve_token(tok, resolve_dir, old_abs) + if resolved is None and moved: + # 被移动文件自身的**出链**(图片等):目标没变但基准目录变了 → 重算写法 + own = resolve_relative(tok, resolve_dir) + if own is not None: + rep = rel_spelling(tok, own, emit_dir) + if rep != tok: + lineno = text.count("\n", 0, m.start()) + 1 + changes.append((lineno, tok, rep)) + out.append(text[pos:m.start()]) + out.append(rep) + pos = m.end() + continue + if resolved is None: + continue + rep = new_spelling(tok, resolved, var_prefix, via_root, emit_dir) + if rep == tok: + continue + lineno = text.count("\n", 0, m.start()) + 1 + changes.append((lineno, tok, rep)) + out.append(text[pos:m.start()]) + out.append(rep) + pos = m.end() + out.append(text[pos:]) + new_text = "".join(out) + + # 改名时:裸文件名引用(#include "x.h"、x.s) + old_name = old_abs.name + new_name = new_abs.name + if old_name != new_name and old_abs != new_abs: + def fix_include(m): + inc = m.group(3) + if pathlib.PurePosixPath(inc.replace("\\", "/")).name == old_name: + base = inc[: len(inc) - len(old_name)] + new_inc = base + new_name + changes.append((text.count("\n", 0, m.start()) + 1, inc, new_inc)) + return m.group(1) + m.group(2) + new_inc + m.group(4) + return m.group(0) + + def fix_filepath(m): + fp = m.group(2) + if pathlib.PurePosixPath(fp.replace("\\", "/")).name == old_name: + base = fp[: len(fp) - len(old_name)] + new_fp = base + new_name + changes.append((text.count("\n", 0, m.start()) + 1, fp, new_fp)) + return m.group(1) + new_fp + m.group(3) + return m.group(0) + + new_text = INCLUDE_RE.sub(fix_include, new_text) + if file_path.suffix.lower() in (".uvprojx", ".uvoptx"): + new_text = FILEPATH_RE.sub(fix_filepath, new_text) + + return new_text + + +def main(argv=None): + ap = argparse.ArgumentParser( + description="改名/挪目录并自动改写全库引用(默认 dry-run)") + ap.add_argument("old", help="旧路径(仓库根相对)") + ap.add_argument("new", help="新路径(仓库根相对;以 / 结尾表示挪进该目录)") + ap.add_argument("--apply", action="store_true", help="真正落盘(默认只预览)") + ap.add_argument("--no-git", action="store_true", help="用 os.rename 代替 git mv") + ap.add_argument("--no-verify", action="store_true", help="跳过 check_doc_links 复核") + args = ap.parse_args(argv) + + old_rel = args.old.replace("\\", "/").rstrip("/") + new_rel = args.new.replace("\\", "/") + if new_rel.endswith("/"): + new_rel = new_rel + old_rel.rsplit("/", 1)[-1] + new_rel = new_rel.rstrip("/") + + if not new_rel or old_rel == new_rel: + print("错误:新旧路径相同或新路径为空") + return 2 + + old_abs = (REPO_ROOT / old_rel).resolve() + new_abs = (REPO_ROOT / new_rel).resolve() + if not old_abs.exists(): + print(f"错误:旧路径不存在:{old_rel}") + return 1 + if new_abs.exists(): + print(f"错误:新路径已存在:{new_rel}") + return 1 + if is_skipped(old_rel): + print(f"错误:{old_rel} 属于第三方/生成物范围,不应由本脚本改写") + return 1 + + old_abs_global[0] = old_abs + new_abs_global[0] = new_abs + + plan = [] + hit_generated = False + for f in tracked_files(): + if is_generated(rel(f)): + if resolve_token(old_rel, f.parent, old_abs)[0] is not None: + hit_generated = True + continue + text, bom, ok = read_text(f) + if not ok: + continue + changes = [] + new_text = rewrite_text(text, f, changes) + if changes: + plan.append((f, text, new_text, bom, changes)) + + n_refs = sum(len(c) for _, _, _, _, c in plan) + print(f"计划:{old_rel} → {new_rel}") + print(f"受影响文件 {len(plan)} 个,引用改写 {n_refs} 处\n") + for f, _, _, _, changes in plan: + print(f"── {rel(f)} ({len(changes)} 处)") + for lineno, o, n in changes[:12]: + print(f" :{lineno} {o} → {n}") + if len(changes) > 12: + print(f" … 其余 {len(changes) - 12} 处略") + print() + + if not args.apply: + print("(dry-run:未做任何修改。确认无误后加 --apply)") + return 0 + + # 1) 先移动文件本身 + new_abs.parent.mkdir(parents=True, exist_ok=True) + if args.no_git: + os.rename(old_abs, new_abs) + else: + r = subprocess.run(["git", "mv", "--", old_rel, new_rel], + cwd=REPO_ROOT, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + if r.returncode != 0: + os.rename(old_abs, new_abs) + print(f"已移动:{old_rel} → {new_rel}") + + # 2) 再改写引用(保 BOM / 换行) + # 被移动文件本身已换位置 → 内容必须写到**新**路径,否则会在旧路径复活一份 + def mapped(f: pathlib.Path) -> pathlib.Path: + fa = f.resolve() + if fa == old_abs: + return new_abs + if str(fa).startswith(str(old_abs) + os.sep): + return new_abs / fa.relative_to(old_abs) + return f + + for f, _, new_text, bom, _ in plan: + target = mapped(f) + target.parent.mkdir(parents=True, exist_ok=True) + data = new_text.encode("utf-8") + if bom: + data = BOM + data + target.write_bytes(data) + print(f"已改写 {len(plan)} 个文件、{n_refs} 处引用") + + # 2b) 收尾提示(脚本管不到的部分) + if hit_generated: + print("ℹ 引用关系图是生成物,未改写 → 重跑 & 'tools\\refgraph\\gen_refgraph.bat'") + if old_abs.suffix.lower() in (".c", ".h") or new_abs.suffix.lower() in (".c", ".h"): + print("ℹ 改了 .c/.h:已同步 uvprojx 与 CMakeLists;若**新增目录**还需把目录加进 " + "uvprojx 的 (见 new-source-file skill),随后跑 @build 验证") + print("ℹ 正文里的**裸文件名/昵称引用**(如「内存方案 §1.4」「app_main.c/h」)无法机械判定," + "已尽量改写,仍建议 grep 一遍旧名:git grep -n '<旧文件名>'") + + # 3) 复核 + if not args.no_verify: + r = subprocess.run([sys.executable, str(REPO_ROOT / "scripts" / "check_doc_links.py"), + "--all"], cwd=REPO_ROOT) + if r.returncode != 0: + print("\n⚠️ check_doc_links 未通过——上面列出的断链需人工确认(多为裸文件名引用)") + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/section_aliases.json b/scripts/section_aliases.json new file mode 100644 index 00000000..d052b5ae --- /dev/null +++ b/scripts/section_aliases.json @@ -0,0 +1,6 @@ +{ + "内存方案": "docs/内存管理/RAM使用分析与优化方案.md", + "架构总览": "docs/架构设计/项目架构总览.md", + "规划": "docs/算法/继电保护功能复现规划.md", + "V0.0": "docs/硬件平台/V0.0_网表详细分析报告.md" +} diff --git a/scripts/stack_report.py b/scripts/stack_report.py index 29bd9b76..55fe3343 100644 --- a/scripts/stack_report.py +++ b/scripts/stack_report.py @@ -46,6 +46,8 @@ ROOTS = { "start_test_task": "testTask", "time_sync_task": "timeSyncTask", "adc_task_func": "adcTask", + "iec104_serv_task": "iec104Task", + "protect_task": "protectTask", } # Runtime lines use the short labels emitted by stack_probe_poll() in freertos.c. @@ -57,6 +59,8 @@ RUNTIME_ALIAS = { "testTask": "test", "timeSyncTask": "timesync", "adcTask": "adc", + "iec104Task": "iec104", + "protectTask": "protect", } FUNC_RE = re.compile( diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 724b2947..a4c1b642 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -104,6 +104,7 @@ set(SYSLOG_SOURCES # 网络配置(双端共享核心 + cfg_file 引擎 + 平台存储后端;MCU 端编 cfg_file_store_mcu.c,见 Keil 工程) set(NETCFG_SOURCES ${NETCFG_DIR}/net_cfg.c + ${NETCFG_DIR}/ftp_cfg.c ${PROJECT_ROOT}/App/cfg/cfg_file.c sim/cfg_file_store_sim.c ) diff --git a/simulator/README.md b/simulator/README.md index f1b278b3..01d9beb0 100644 --- a/simulator/README.md +++ b/simulator/README.md @@ -282,4 +282,4 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI设计/UI_自动化 > **模拟器跑过 ≠ 真机跑过**:本目录与真机的**全部已知差异**(网络 pending 机制缺失、代次号恒 0、 > 日志/配置的 mtime 粒度与堆后端差异、IWDG/NAND/RTC 等硬件不可建模)统一登记在 > [`../docs/测试说明/模拟器差距清单.md`](../docs/测试说明/模拟器差距清单.md)—— -> 改动触及 `sim/` 任何后端时,**必须同步在那里登记一行**;改动前先看该文档 §3 判断是否必须回真机。 +> 改动触及 `sim/` 任何后端时,**必须同步在那里登记一行**;改动前先看 `docs/测试说明/模拟器差距清单.md` §3 判断是否必须回真机。 diff --git a/simulator/build_sim.ps1 b/simulator/build_sim.ps1 index 277b30f0..606b6b77 100644 --- a/simulator/build_sim.ps1 +++ b/simulator/build_sim.ps1 @@ -141,4 +141,4 @@ if (Test-Path $exe) { } Write-Host "[build_sim] 构建退出码 0 但未找到 $exe" -ForegroundColor Red exit 1 - \ No newline at end of file + diff --git a/simulator/dbg_log.h b/simulator/dbg_log.h index ec2153d4..6d0e51af 100644 --- a/simulator/dbg_log.h +++ b/simulator/dbg_log.h @@ -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 */ diff --git a/simulator/lv_conf.h b/simulator/lv_conf.h index 0294ed29..27666afd 100644 --- a/simulator/lv_conf.h +++ b/simulator/lv_conf.h @@ -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 diff --git a/simulator/main.c b/simulator/main.c index a979476f..670efcb3 100644 --- a/simulator/main.c +++ b/simulator/main.c @@ -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) { diff --git a/simulator/sim/net_socket_sim.c b/simulator/sim/net_socket_sim.c index 4817f35e..47ce668f 100644 --- a/simulator/sim/net_socket_sim.c +++ b/simulator/sim/net_socket_sim.c @@ -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; @@ -347,6 +335,55 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) return nidx; } +/* + * 函数功能:查询监听 Socket 是否有可接受的连接(与 MCU 端同名 API) + * 入口参数:sockfd - 监听 socket 描述符 + * 返回值:1 - 可尝试 accept;0 - 描述符无效 + * 限定条件:socket 已创建 + * 函数说明:Winsock 后端无"零成本预知"手段(要嘛真调 accept、要嘛 select()), + * 而 accept 本就是非阻塞的、**没有 MCU 端的 RPC 成本** → 恒返回 1, + * 使上层门控在模拟器上等价于"改前行为"(逐位一致,不引入新语义) + */ +int net_can_accept(int sockfd) { + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + return 0; + } + return 1; +} + +/* + * 函数功能:查询监听 Socket 是否处于"应该尝试 accept"的就绪态(与 MCU 端同名 API) + * 入口参数:sockfd - 监听 socket 描述符 + * 返回值:1 - 可尝试 accept;0 - 描述符无效 + * 限定条件:socket 已创建 + * 函数说明:同 net_can_accept——Winsock accept 本无 RPC 成本 → 恒返回 1, + * 使上层门控在模拟器上等价于"改前行为"。 + * ⚠ MCU 端该函数额外放行 CLOSED+auto_relisten(单连接复用的断连待重建态), + * 那是芯片 fd 别名模型的产物;Winsock 后端 fd 与监听器分离 + * (设计文档 D6:双模型分裂暂不消除),无此中间态,恒 1 即为正确语义。 + */ +int net_listener_armed(int sockfd) { + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + return 0; + } + return 1; +} + +/* + * 函数功能:查询 Socket 是否"值得发一次 recv"(与 MCU 端同名 API) + * 入口参数:sockfd - socket 描述符 + * 返回值:1 - 可尝试 recv;0 - 描述符无效 + * 限定条件:socket 已创建 + * 函数说明:同 net_can_accept——Winsock recv 非阻塞、无 RPC 成本 → 恒返回 1, + * 上层门控在模拟器上不改变既有行为(含"每轮都 recv"的自愈特性) + */ +int net_can_read(int sockfd) { + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + return 0; + } + return 1; +} + /* * 函数功能:TCP Client 发起连接 * 入口参数:sockfd - socket 描述符 @@ -357,8 +394,7 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) * 限定条件: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) { @@ -397,8 +433,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) { @@ -440,8 +475,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; @@ -486,8 +520,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; @@ -530,8 +563,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; @@ -573,8 +605,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; @@ -603,8 +634,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; @@ -623,8 +653,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; } @@ -636,8 +665,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; @@ -668,8 +696,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) { @@ -691,8 +718,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); } @@ -702,8 +728,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); } @@ -713,8 +738,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); } @@ -724,8 +748,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); } @@ -736,8 +759,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; } @@ -757,8 +779,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; diff --git a/simulator/sim/sim_port.c b/simulator/sim/sim_port.c index 3bd765cf..c29377e3 100644 --- a/simulator/sim/sim_port.c +++ b/simulator/sim/sim_port.c @@ -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; } diff --git a/simulator/sim/sim_port.h b/simulator/sim/sim_port.h index 3455938d..025cb7ef 100644 --- a/simulator/sim/sim_port.h +++ b/simulator/sim/sim_port.h @@ -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 @@ -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 */ diff --git a/simulator/sim/sim_syslog.c b/simulator/sim/sim_syslog.c index 54386609..6221e2ce 100644 --- a/simulator/sim/sim_syslog.c +++ b/simulator/sim/sim_syslog.c @@ -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; } diff --git a/simulator/sim/ui_net_sink_backend_sim.c b/simulator/sim/ui_net_sink_backend_sim.c index e24a21d8..64f76f77 100644 --- a/simulator/sim/ui_net_sink_backend_sim.c +++ b/simulator/sim/ui_net_sink_backend_sim.c @@ -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; diff --git a/simulator/tests/check_content_pages.py b/simulator/tests/check_content_pages.py index b1d72f2e..aa8c5405 100644 --- a/simulator/tests/check_content_pages.py +++ b/simulator/tests/check_content_pages.py @@ -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): diff --git a/simulator/tests/check_menu_rows.py b/simulator/tests/check_menu_rows.py index 1655d27d..989e413a 100644 --- a/simulator/tests/check_menu_rows.py +++ b/simulator/tests/check_menu_rows.py @@ -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): diff --git a/test/board/ch395f_reuse_bench.c b/test/board/ch395f_reuse_bench.c new file mode 100644 index 00000000..1aa9540c --- /dev/null +++ b/test/board/ch395f_reuse_bench.c @@ -0,0 +1,374 @@ +/** + * @file ch395f_reuse_bench.c + * @brief N-30 芯片级 Socket 复用时序台架(裸机,内核前运行,OS/net/FTP 全剥离) + * @details 直接调用 ch395f_* 命令,循环执行受控时序矩阵(延迟/关闭次序/载荷/Socket 号), + * 每步打印芯片状态与中断寄存器,定位"关→开→监听→连接"复用时序中 + * 芯片行为的确切规律。配套主机脚本 test/host/ch395f_reuse_host.py。 + * 定案结论与实验矩阵演进 → CH395F_Trap_Records.md Trap 25。 + * + * 存档状态:本文件在主分支**不注册编译**(未加入 uvprojx),仅作实验工具存档; + * 运行方法:在实验分支(如 n30-chip-bench)把本 .c 注册进 uvprojx Test 组、 + * Src/main.c 的 USER CODE 2 区在 app_main_init() 之前调 ch395f_reuse_bench() + * (内核前裸机循环,永不返回,断电退出),烧录后跑主机脚本对照芯片日志。 + * 当前矩阵为定案轮(对照 vs "丢 CONNECT 捕获自 accept"修复组),见 s_vars[]。 + * + * close_mode:0=主机先关(芯片等 DISCONNECT);1=芯片先关;2=无数据连上即关 + * verify:0=无修复;1=listen 后确认 LISTEN 重试;2=缓冲重分配; + * 3=幽灵耐心等;4=等 CONNECT 期轮询状态+自 accept(定案修复) + */ + +#include "ch395f_reuse_bench.h" + +#if N30_CHIP_BENCH + +#include "ch395f.h" +#include "dbg_log.h" +#include "stm32f4xx_hal.h" + +/* 变体表:{sock, close 后复开延迟 ms, TX 字节数, close_mode, verify_listen} */ +typedef struct { + uint8_t sock; + uint16_t delay_ms; + uint16_t tx_len; + uint8_t close_mode; + uint8_t verify; /* 1 = listen 后确认 TCP==0x01,否则 close 重试(修复假设) */ +} bench_var_t; + +static const bench_var_t s_vars[] = { + { 3, 0, 100, 0, 0 }, /* 0ms 对照 */ + { 3, 300, 100, 0, 0 }, /* 300ms 对照 */ + { 3, 800, 2000, 0, 0 }, /* 2KB 对照 */ + { 3, 0, 100, 0, 4 }, /* 0ms + 丢CONNECT捕获自accept */ + { 3, 300, 100, 0, 4 }, /* 300ms + 丢CONNECT捕获 */ + { 3, 800, 2000, 0, 4 }, /* 2KB + 丢CONNECT捕获 */ +}; + +#define BENCH_N_VARS (sizeof(s_vars) / sizeof(s_vars[0])) +#define BENCH_PORT(i) (5000U + (uint16_t)(i)) + +/* TX 图案缓冲(静态,SRAM;2KB = 最大变体载荷) */ +static uint8_t s_tx_pat[2048]; +/* RX 排空缓冲 */ +static uint8_t s_rx_buf[2048]; + +/* + * 函数功能:轮询本 Socket 中断(读全局→读本 Socket,读即清) + * 入口参数:sock - Socket 号 uint8_t + * timeout - 超时 ms uint32_t + * out_bits - 输出最后一次读到的 SINT 位 uint8_t * + * 返回值:1=拿到中断 0=超时 int + */ +static int bench_wait_sock_int(uint8_t sock, uint32_t timeout, uint8_t *out_bits) { + uint32_t t0 = HAL_GetTick(); + while ((uint32_t)(HAL_GetTick() - t0) < timeout) { + uint16_t glob = ch395f_get_glob_int_status_all(); + if ((glob & (1U << (sock + 4U))) != 0U) { + uint8_t si = ch395f_get_sock_int_status(sock); + if (out_bits != 0) { + *out_bits = si; + } + return 1; + } + HAL_Delay(10); + } + return 0; +} + +/* 打印芯片两字节状态(Socket 态 / TCP 态) */ +static void bench_log_state(const char *tag, uint8_t sock) { + uint8_t st[2] = { 0, 0 }; + ch395f_get_socket_status(sock, st); + DBG_INFO("BENCH %s sock%u st=0x%02X/0x%02X t=%u", tag, (unsigned)sock, + st[0], st[1], (unsigned)HAL_GetTick()); +} + +/* 芯片级初始化(复刻 net_init 序列,剥离全部内核 API) */ +static int bench_chip_init(void) { + if (ch395f_check_exist() != CH395F_STATUS_OK) { + DBG_ERROR("BENCH chip not found"); + return -1; + } + ch395f_reset(); + /* 等 PHY link(Trap 09:link up 前 open 必失败) */ + for (int i = 0; i < 100; i++) { + if (ch395f_get_phy_status() != 0U) { + break; + } + HAL_Delay(100); + } + /* bit3=1 SOCKET_CLOSE;bit0 保留必须为 0(§5.52)→ 与生产 net_init 同构 */ + ch395f_set_fun_para(0x08); + ch395f_set_tcp_mss(CH395F_TCP_MSS); + ch395f_set_retrans_count(3); + ch395f_set_retrans_period(200); + ch395f_set_keepalive_idle(60000); + ch395f_set_keepalive_intvl(5000); + ch395f_set_keepalive_cnt(3); + /* 静态 IP(与生产一致) */ + { + uint8_t ip[4] = { 192, 168, 1, 100 }; + uint8_t gw[4] = { 192, 168, 1, 1 }; + uint8_t msk[4] = { 255, 255, 255, 0 }; + ch395f_set_ip_addr(ip); + ch395f_set_gwip_addr(gw); + ch395f_set_mask_addr(msk); + } + if (ch395f_init() != CH395F_STATUS_OK) { + DBG_ERROR("BENCH ch395f_init failed"); + return -1; + } + /* 8 Socket 缓冲布局(与生产一致) */ + for (int i = 7; i >= 0; i--) { + uint8_t recv_start = (uint8_t)(i * 6); + ch395f_set_send_buf((uint8_t)i, (uint8_t)(recv_start + 4), 2); + ch395f_set_recv_buf((uint8_t)i, recv_start, 4); + } + ch395f_ping_enable(1); + return 0; +} + +/* 单轮实验 */ +static void bench_round(unsigned idx) { + const bench_var_t *v = &s_vars[idx]; + uint8_t sock = v->sock; + uint16_t port = BENCH_PORT(idx); + uint8_t si = 0; + /* 幽灵等待期已见 CONNECT 的标志(跳过后续 CONNECT 等待) */ + int pre_connected = 0; + + DBG_INFO("BENCH === r%u sock%u port%u delay%u tx%u mode%u t=%u", + idx, (unsigned)sock, (unsigned)port, (unsigned)v->delay_ms, + (unsigned)v->tx_len, (unsigned)v->close_mode, (unsigned)HAL_GetTick()); + + /* 1. open + listen(Trap 09:close 后需延时重开,失败重试 ×3) */ + ch395f_set_proto_type(sock, CH395F_PROTO_TYPE_TCP); + ch395f_set_sour_port(sock, port); + uint8_t ost = CH395F_ERR_SUCCESS; + for (int open_try = 0; open_try < 3; open_try++) { + ost = ch395f_open_socket(sock); + if (ost == CH395F_ERR_SUCCESS) { + break; + } + DBG_INFO("BENCH open retry%d err=0x%02X t=%u", open_try, ost, (unsigned)HAL_GetTick()); + ch395f_close_socket(sock); + HAL_Delay(500); + } + if (ost != CH395F_ERR_SUCCESS) { + DBG_ERROR("BENCH open fail 0x%02X", ost); + return; + } + uint8_t lst = ch395f_tcp_listen(sock); + if (lst != CH395F_ERR_SUCCESS) { + DBG_ERROR("BENCH listen fail 0x%02X", lst); + ch395f_close_socket(sock); + return; + } + /* 缓冲重分配变体:listen 后重设收发缓冲(隔离"缓冲管理残留"假设) */ + if (v->verify == 2U) { + uint8_t recv_start = (uint8_t)(sock * 6); + ch395f_clear_recv_buf(sock); + ch395f_set_send_buf(sock, (uint8_t)(recv_start + 4), 2); + ch395f_set_recv_buf(sock, recv_start, 4); + DBG_INFO("BENCH realloc done sock%u t=%u", (unsigned)sock, (unsigned)HAL_GetTick()); + } + /* 清残留中断(与生产 listen 后一致) */ + for (int j = 0; j < 8; j++) { + (void)ch395f_get_sock_int_status((uint8_t)j); + } + (void)ch395f_get_glob_int_status_all(); + bench_log_state("post-listen", sock); + + /* 修复假设验证:listen 后 TCP 态必须真正 == 0x01(LISTEN); + * mode1:close 快速重试;mode3:耐心等幽灵消退(5s,每 100ms 复查), + * 期间幽灵→LISTEN 转变会补发 CONNECT 中断则直接放行 */ + if (v->verify == 1U || v->verify == 3U) { + uint32_t budget = (v->verify == 3U) ? 5000U : 1000U; + int att_max = (v->verify == 3U) ? 2 : 4; + for (int att = 0; att < att_max; att++) { + uint8_t st[2] = { 0, 0 }; + uint32_t tw = HAL_GetTick(); + int connect_fired = 0; + /* 等 TCP 进 LISTEN(幽灵消退)或 CONNECT 中断补发 */ + while ((uint32_t)(HAL_GetTick() - tw) < budget) { + uint16_t glob = ch395f_get_glob_int_status_all(); + if ((glob & (1U << (sock + 4U))) != 0U) { + uint8_t si = ch395f_get_sock_int_status(sock); + if ((si & CH395F_SINT_STAT_CONNECT) != 0U) { + connect_fired = 1; + DBG_INFO("BENCH verify CONNECT during ghost-wait att%d t=%u", + att, (unsigned)HAL_GetTick()); + break; + } + } + ch395f_get_socket_status(sock, st); + if (st[1] == 0x01U) { + break; + } + HAL_Delay(100); + } + DBG_INFO("BENCH verify att%d st=0x%02X/0x%02X cf=%d t=%u", + att, st[0], st[1], connect_fired, (unsigned)HAL_GetTick()); + if (connect_fired) { + /* 幽灵期主机已连上:跳过 CONNECT 等待直接进流程 */ + pre_connected = 1; + break; + } + if (st[1] == 0x01U) { + break; + } + /* 幽灵未消:close → 50ms → 重新 open+listen(mode3 只试一次) */ + ch395f_close_socket(sock); + HAL_Delay(50); + ch395f_open_socket(sock); + ch395f_tcp_listen(sock); + for (int j = 0; j < 8; j++) { + (void)ch395f_get_sock_int_status((uint8_t)j); + } + (void)ch395f_get_glob_int_status_all(); + } + } + + /* 2. 等 CONNECT(10s;幽灵等待期已见 CONNECT 则跳过) + * v6 关键探针:等待期每 500ms 读芯片状态;若已 ESTABLISHED(0x04) + * 说明握手完成但 CONNECT 中断丢失 → 补做 accept(对账②模拟) */ + int got = pre_connected; + uint32_t t0 = HAL_GetTick(); + while (!got && (uint32_t)(HAL_GetTick() - t0) < 10000U) { + if (bench_wait_sock_int(sock, 100, &si)) { + if ((si & CH395F_SINT_STAT_CONNECT) != 0U) { + got = 1; + break; + } + DBG_INFO("BENCH unexpected int=0x%02X while wait", si); + } + if (v->verify == 4U) { + uint8_t st[2] = { 0, 0 }; + ch395f_get_socket_status(sock, st); + if ((uint32_t)(HAL_GetTick() - t0) % 1000U < 110U) { + DBG_INFO("BENCH wait-state st=0x%02X/0x%02X t=%u", + st[0], st[1], (unsigned)HAL_GetTick()); + } + if (st[1] == 0x04U) { + DBG_INFO("BENCH LOST-CONNECT caught: state ESTABLISHED, self-accept t=%u", + (unsigned)HAL_GetTick()); + got = 1; + break; + } + } + } + if (!got) { + DBG_ERROR("BENCH no CONNECT within 10s (host not synced?)"); + ch395f_close_socket(sock); + HAL_Delay(v->delay_ms); + return; + } + bench_log_state("post-accept", sock); + /* 对端信息 + RX 残渣检查(旧连接残留的直接证据) */ + { + uint8_t rem[6] = { 0 }; + ch395f_get_remot_ipp(sock, rem); + uint16_t rl = ch395f_get_recv_len(sock); + DBG_INFO("BENCH remote port=%u recv_len=%u", + (unsigned)(rem[4] | (rem[5] << 8)), (unsigned)rl); + if (rl > 0U) { + uint16_t n = (rl > 2048U) ? 2048U : rl; + ch395f_read_recv_buf(sock, s_rx_buf, n); + DBG_INFO("BENCH drained %u residue bytes", (unsigned)n); + } + } + + /* 3. TX 探针(1KB 分块,Trap 20) */ + if (v->tx_len > 0U) { + uint16_t sent = 0; + while (sent < v->tx_len) { + uint16_t chunk = (uint16_t)(v->tx_len - sent); + if (chunk > 1024U) { + chunk = 1024U; + } + ch395f_write_send_buf(sock, &s_tx_pat[sent], chunk); + sent += chunk; + /* 等 SENBUF_FREE(500ms) */ + if (sent < v->tx_len) { + uint32_t tw = HAL_GetTick(); + while ((uint32_t)(HAL_GetTick() - tw) < 500U) { + if (bench_wait_sock_int(sock, 50, &si)) { + if ((si & CH395F_SINT_STAT_SENBUF_FREE) != 0U) { + break; + } + } + } + } + } + DBG_INFO("BENCH tx %u written t=%u", (unsigned)v->tx_len, (unsigned)HAL_GetTick()); + /* 连接存活观察窗:500ms 内每 50ms 记状态+中断(抓"秒杀") */ + for (int k = 0; k < 10; k++) { + uint16_t glob = ch395f_get_glob_int_status_all(); + if ((glob & (1U << (sock + 4U))) != 0U) { + si = ch395f_get_sock_int_status(sock); + DBG_INFO("BENCH int during window=0x%02X t=%u", si, (unsigned)HAL_GetTick()); + } + HAL_Delay(50); + bench_log_state("window", sock); + } + } + + /* 4. 关闭次序 */ + if (v->close_mode == 1U) { + /* 芯片先关 */ + HAL_Delay(300); + ch395f_close_socket(sock); + DBG_INFO("BENCH chip closed first t=%u", (unsigned)HAL_GetTick()); + } else { + /* 等主机关(DISCONNECT 事件,5s) */ + uint32_t tw = HAL_GetTick(); + int disc = 0; + while ((uint32_t)(HAL_GetTick() - tw) < 5000U) { + if (bench_wait_sock_int(sock, 100, &si)) { + if ((si & (CH395F_SINT_STAT_DISCONNECT | CH395F_SINT_STAT_SOCK_TIMEOUT)) != 0U) { + disc = 1; + break; + } + } + } + DBG_INFO("BENCH host-close seen=%d t=%u", disc, (unsigned)HAL_GetTick()); + ch395f_close_socket(sock); + } + + /* 5. 关闭后拆链观察:1s 内每 50ms 记状态 + 中断(Trap 14 滞后中断取证) */ + for (int k = 0; k < 20; k++) { + uint16_t glob = ch395f_get_glob_int_status_all(); + uint8_t late = 0; + if ((glob & (1U << (sock + 4U))) != 0U) { + late = ch395f_get_sock_int_status(sock); + } + if (late != 0U) { + DBG_INFO("BENCH late-int=0x%02X t=%u", late, (unsigned)HAL_GetTick()); + } + if ((k % 4) == 0) { + bench_log_state("post-close", sock); + } + HAL_Delay(50); + } + + /* 6. 受控延迟后进入下一轮 */ + HAL_Delay(v->delay_ms); +} + +void ch395f_reuse_bench(void) { + DBG_INFO("BENCH ===== N30 chip-level reuse bench START ====="); + if (bench_chip_init() != 0) { + DBG_ERROR("BENCH init failed, giving up (normal boot continues)"); + return; + } + DBG_INFO("BENCH chip ready, cycling %u variants forever", (unsigned)BENCH_N_VARS); + for (unsigned i = 0; i < 2048U; i++) { + s_tx_pat[i] = (uint8_t)(0xA5U + (i % 251U)); + } + unsigned idx = 0; + for (;;) { + bench_round(idx); + idx = (idx + 1U) % (unsigned)BENCH_N_VARS; + } +} + +#endif /* N30_CHIP_BENCH */ diff --git a/test/board/ch395f_reuse_bench.h b/test/board/ch395f_reuse_bench.h new file mode 100644 index 00000000..09694d65 --- /dev/null +++ b/test/board/ch395f_reuse_bench.h @@ -0,0 +1,13 @@ +/* + * ch395f_reuse_bench.h - N-30 芯片级 Socket 复用时序台架(裸机实验,临时分支专用) + */ + +#ifndef CH395F_REUSE_BENCH_H +#define CH395F_REUSE_BENCH_H + +/* 实验总开关:1 = 内核前进入裸机台架(OS 永不启动,断电退出) */ +#define N30_CHIP_BENCH 1 + +void ch395f_reuse_bench(void); + +#endif /* CH395F_REUSE_BENCH_H */ diff --git a/test/board/ch395f_test_task.c b/test/board/ch395f_test_task.c index 32bcbe09..e43d6d85 100644 --- a/test/board/ch395f_test_task.c +++ b/test/board/ch395f_test_task.c @@ -619,6 +619,289 @@ static void phase2_tc202(void) } } +/* =================================================================== + * TC-NET-204(硬件层):Trap 07 定案对照——三种关闭次序 A/B/C + * + * 背景:Trap 07(2025-07)断言"`disconnect → close` 会让 Socket 卡 `FIN_WAIT_2` + * 数分钟、无法重开,因为 close 不再发 RST"。该断言有两处可疑: + * ① 手册无 RST 语义——§5.35 只说"关闭 Socket 前 CH395 会自动断开 TCP 连接", + * 全文 RST 只出现在错误码 CH395_ERR_RST 与 RST/RSTI 引脚; + * ② "FIN_WAIT_2"是把 GET_SOCKET_STATUS 第 2 字节当独立事实读出来的—— + * §5.22 第 1 字节只有 00H(CLOSED)/05H(OPEN) 两个取值,第 2 字节才是 TCP 态, + * 而官方 SDK CH395CMDGetSocketStatus 注释明确:"Second byte — TCP state, + * **meaningful only if** TCP mode **and the first byte is open**"。 + * 当年 Phase 2 收尾正是"byte0 已 CLOSED 才 break,然后照样打印 byte1"。 + * 另:git 全历史里 ch395f_tcp_disconnect() 只有定义、零调用点,"disconnect→close" + * 序列从未进入过任何提交代码 → 现象来自一次未留痕的试验。 + * + * 受控对照(每模式 x PHASE204_LOOP_COUNT 轮,同一 Socket、同一 PC 回显服务): + * 模式 A:close only(现行做法) + * 模式 B:tcp_disconnect → close(Trap 07 声称会卡死的序列) + * 模式 C:tcp_disconnect only,不 close(检验 FUN_PARA bit3=1 下芯片会不会自己关) + * 观测量: + * ① 回到 SOCKET_CLOSED(byte0=00H) 的耗时;② 该瞬间 byte1 的读数(验证"无意义"口径) + * ③ 重开 + 重连 + 回显是否成功(Trap 07 的"无法重新打开") + * ④ 重开会话首次读中断是否命中上一条连接残留的 DISCONNECT(Trap 14 轴) + * ⚠ 关闭与重开之间**不主动清中断**(与 2025-07 现场一致),残留事件由重开后的 + * 首次中断读取现场暴露(读即清,无法窥视)。 + * =================================================================== */ +#ifndef PHASE204_LOOP_COUNT +#define PHASE204_LOOP_COUNT 5 +#endif +#ifndef PHASE204_LOCAL_PORT +#define PHASE204_LOCAL_PORT 50100 /* 避开 201/202/203 用过的本地端口 */ +#endif +#ifndef PHASE204_CLOSE_WAIT_MS +#define PHASE204_CLOSE_WAIT_MS 10000 /* 回到 CLOSED 的观察窗 */ +#endif + +/* 建立一次会话:open + connect + 等 CONNECT + 64B 回显比对 + * 入口参数:sock - Socket 号;lport - 本地端口;mode/round - 仅用于日志定位 + * 返回值:1=会话成功(含回显比对通过) 0=任一步失败 uint8_t + * 说明:等待 CONNECT 期间读到的其它中断(含上一条连接残留的 DISCONNECT) + * 现场打印,用于 Trap 14"滞后锁存"取证。 */ +static uint8_t tc204_session(uint8_t sock, uint16_t lport, uint8_t mode, uint8_t round) +{ + uint8_t tx[64]; + uint8_t rx[300]; + uint8_t ok = 0; + uint8_t r; + uint8_t match; + uint16_t rl; + uint16_t n; + uint32_t tick; + int32_t k; + + for (k = 0; k < 64; k++) { + tx[k] = (uint8_t)k; + } + + ch395f_set_sour_port(sock, lport); + r = ch395f_open_socket(sock); + if (r != CH395F_ERR_SUCCESS) { + DBG_INFO("TC204 m%u r%u open ret=0x%02X", (unsigned)mode, (unsigned)round, r); + return 0; + } + r = ch395f_tcp_connect(sock); + if (r != CH395F_ERR_SUCCESS) { + DBG_INFO("TC204 m%u r%u connect cmd ret=0x%02X", (unsigned)mode, (unsigned)round, r); + ch395f_close_socket(sock); + return 0; + } + + /* 等 CONNECT 中断(期间其它中断现场打印 = 残留事件取证点) */ + tick = HAL_GetTick(); + while (HAL_GetTick() - tick < PHASE2_TIMEOUT_MS) { + uint16_t gl = ch395f_get_glob_int_status_all(); + if ((gl & (1U << (sock + 4))) != 0U) { + uint8_t si = ch395f_get_sock_int_status(sock); + if ((si & CH395F_SINT_STAT_CONNECT) != 0U) { + ok = 1; + break; + } + if (si != 0U) { + DBG_INFO("TC204 m%u r%u stale-int=0x%02X at +%ums", + (unsigned)mode, (unsigned)round, si, + (unsigned)(HAL_GetTick() - tick)); + } + } + HAL_Delay(10); + } + if (!ok) { + DBG_INFO("TC204 m%u r%u no CONNECT within %ums", (unsigned)mode, (unsigned)round, + (unsigned)PHASE2_TIMEOUT_MS); + ch395f_close_socket(sock); + return 0; + } + HAL_Delay(100); + + /* 64B 发送 + SENBUF_FREE + 回显比对(证明这是一条真实连接) */ + ch395f_write_send_buf(sock, tx, 64); + tick = HAL_GetTick(); + ok = 0; + while (HAL_GetTick() - tick < 2000) { + uint16_t gl = ch395f_get_glob_int_status_all(); + if ((gl & (1U << (sock + 4))) != 0U) { + uint8_t si = ch395f_get_sock_int_status(sock); + if ((si & CH395F_SINT_STAT_SENBUF_FREE) != 0U) { + ok = 1; + break; + } + } + HAL_Delay(10); + } + if (!ok) { + DBG_INFO("TC204 m%u r%u no SENBUF_FREE", (unsigned)mode, (unsigned)round); + ch395f_close_socket(sock); + return 0; + } + + tick = HAL_GetTick(); + while (HAL_GetTick() - tick < 10000) { + uint16_t rlen = ch395f_get_recv_len(sock); + if (rlen > 0U) { + rl = (rlen > (uint16_t)sizeof(rx)) ? (uint16_t)sizeof(rx) : rlen; + ch395f_read_recv_buf(sock, rx, rl); + n = (rl < 64U) ? rl : 64U; + match = (rl >= 64U) ? 1U : 0U; + for (k = 0; k < (int32_t)n; k++) { + if (rx[k] != tx[k]) { + match = 0; + break; + } + } + if (!match) { + DBG_INFO("TC204 m%u r%u echo MISMATCH (rlen=%u)", (unsigned)mode, + (unsigned)round, (unsigned)rlen); + } + return match; + } + HAL_Delay(20); + } + DBG_INFO("TC204 m%u r%u echo NOT rcvd", (unsigned)mode, (unsigned)round); + ch395f_close_socket(sock); + return 0; +} + +/* 执行一种关闭次序,并测量"回到 SOCKET_CLOSED 的耗时" + * 入口参数:sock - Socket 号;mode - 0=A close / 1=B disconnect→close / 2=C disconnect only + * round - 日志定位 + * 返回值:1=观察窗内 byte0 回到 CLOSED 0=未回到 uint8_t + * 说明:关闭与轮询之间不读中断(读即清会抹掉 Trap 14 的滞后事件现场)。 */ +static uint8_t tc204_close_seq(uint8_t sock, uint8_t mode, uint8_t round) +{ + /* 状态采样里程碑(ms):抓 byte0/byte1 的演变 */ + static const uint16_t s_marks[] = { 0U, 50U, 100U, 250U, 500U, 1000U, 2000U, 5000U, 10000U }; + uint8_t st[2] = { 0, 0 }; + uint32_t tick; + uint32_t el = 0U; + uint8_t closed = 0U; + uint8_t mi = 0U; + uint8_t n_marks = (uint8_t)(sizeof(s_marks) / sizeof(s_marks[0])); + + if (mode >= 1U) { + uint8_t r = ch395f_tcp_disconnect(sock); + DBG_INFO("TC204 m%u r%u disconnect ret=0x%02X", (unsigned)mode, (unsigned)round, r); + } + if (mode <= 1U) { + uint8_t r = ch395f_close_socket(sock); + DBG_INFO("TC204 m%u r%u close ret=0x%02X", (unsigned)mode, (unsigned)round, r); + } + + tick = HAL_GetTick(); + while (el <= (uint32_t)PHASE204_CLOSE_WAIT_MS) { + ch395f_get_socket_status(sock, st); + el = (uint32_t)(HAL_GetTick() - tick); + while ((mi < n_marks) && (el >= s_marks[mi])) { + DBG_INFO("TC204 m%u r%u poll +%ums st=0x%02X/0x%02X", (unsigned)mode, + (unsigned)round, (unsigned)s_marks[mi], st[0], st[1]); + mi++; + } + if (st[0] == CH395F_SOCKET_CLOSED) { + closed = 1U; + break; + } + HAL_Delay(20); + } + /* 判定瞬间再读一次:byte0 已 CLOSED 时 byte1 按 §5.22/官方注释属"无意义"区 */ + ch395f_get_socket_status(sock, st); + if (closed != 0U) { + DBG_INFO("TC204 m%u r%u CLOSED at +%ums (byte0=0x%02X byte1=0x%02X)", + (unsigned)mode, (unsigned)round, (unsigned)el, st[0], st[1]); + } else { + DBG_INFO("TC204 m%u r%u NOT-CLOSED within %ums (byte0=0x%02X byte1=0x%02X)", + (unsigned)mode, (unsigned)round, (unsigned)PHASE204_CLOSE_WAIT_MS, + st[0], st[1]); + } + return closed; +} + +/* 轮末收尾:关闭本轮第二条连接并等回 CLOSED + * 说明:芯片不允许对已 OPEN 的 Socket 再 open(返回 0x20 FAIL)——不收尾则下一轮 + * 首轮 open 必失败并级联(2026-10-11 TC204 首跑实测踩过)。 */ +static void tc204_round_teardown(uint8_t sock) +{ + uint8_t st[2] = { 0, 0 }; + uint32_t tick; + + ch395f_close_socket(sock); + tick = HAL_GetTick(); + while (HAL_GetTick() - tick < 3000) { + ch395f_get_socket_status(sock, st); + if (st[0] == CH395F_SOCKET_CLOSED) { + break; + } + HAL_Delay(20); + } + HAL_Delay(200); +} + +/* 单轮:会话 → 关闭次序 → 重开会话 */ +static void phase2_tc204_once(uint8_t mode, uint8_t round) +{ + uint8_t sock = 0; + uint16_t lport = (uint16_t)(PHASE204_LOCAL_PORT + (uint16_t)(mode * 16U + round)); + uint8_t s1; + uint8_t s2; + uint8_t closed; + + DBG_INFO("TC204 === m%u r%u lport %u ===", (unsigned)mode, (unsigned)round, + (unsigned)lport); + + /* 1. 建立真实连接(有连接才谈得上 FIN_WAIT_*) */ + s1 = tc204_session(sock, lport, mode, round); + TEST_CHECK(s1, "TC204 m%u r%u session1 (connect+echo)", (unsigned)mode, (unsigned)round); + if (!s1) { + tc204_round_teardown(sock); + return; + } + + /* 2. 关闭次序 + 回到 CLOSED 的耗时 */ + closed = tc204_close_seq(sock, mode, round); + if (mode == 2U) { + /* 模式 C 的预期恰恰相反:bit3=1(§5.52 ②)下芯片不得自行关闭 Socket */ + TEST_CHECK(!closed, "TC204 m2 r%u stays OPEN after disconnect (FUN_PARA bit3)", + (unsigned)round); + if (!closed) { + /* Trap 07 的"无法重新打开"现场:不 close 直接 open,看芯片返回什么 */ + uint8_t r = ch395f_open_socket(sock); + DBG_INFO("TC204 m2 r%u open-without-close ret=0x%02X", (unsigned)round, r); + /* 收尾:显式 close 并测耗时(证明 close 才是把 Socket 拉回 CLOSED 的动作) */ + (void)tc204_close_seq(sock, 0U, round); + } + } else { + TEST_CHECK(closed, "TC204 m%u r%u back to CLOSED within %ums", (unsigned)mode, + (unsigned)round, (unsigned)PHASE204_CLOSE_WAIT_MS); + } + + /* 3. 重开 + 重连 + 回显(Trap 07 的"无法重新打开"判据;Trap 09 沉降 200ms) */ + HAL_Delay(200); + s2 = tc204_session(sock, (uint16_t)(lport + 64U), mode, round); + TEST_CHECK(s2, "TC204 m%u r%u reopen+reconnect+echo OK", (unsigned)mode, (unsigned)round); + tc204_round_teardown(sock); +} + +static void phase2_tc204(void) +{ + uint8_t sock = 0; + uint8_t ip_arr[4]; + uint8_t m; + uint8_t i; + + test_parse_ip(PHASE2_REMOTE_IP, ip_arr); + + /* 协议 / 目标在整组内保持不变,仅每轮换本地端口 */ + ch395f_set_proto_type(sock, CH395F_PROTO_TYPE_TCP); + ch395f_set_des_ip(sock, ip_arr); + ch395f_set_des_port(sock, PHASE2_REMOTE_PORT); + + for (m = 0; m < 3U; m++) { + for (i = 0; i < PHASE204_LOOP_COUNT; i++) { + phase2_tc204_once(m, i); + } + } +} + /* TC-NET-203(硬件层):TCP Client 连接超时(负向测试,P1) * * 前置环境:PC 未在 192.168.1.2:8081 提供服务(与 TC-201/202 互斥, @@ -715,11 +998,14 @@ static void phase2_tc203(void) /* 反核实(Trap 14 第二道防线):负向事件与实际状态矛盾时以状态为准。 * 若收到"负向事件"但 socket 实际已 ESTABLISHED,说明刚才读到的是 - * 上一会话遗留的陈旧 DISCONNECT,真实结果是连接成功 —— 环境错误。 */ + * 上一会话遗留的陈旧 DISCONNECT,真实结果是连接成功 —— 环境错误。 + * ⚠ §5.22:byte0 只有 00H(CLOSED)/05H(OPEN),TCP 态在 **byte1**; + * 拿 byte0 比 TCP_ESTABLISHED(0x04) 恒假(2026-10-11 审计修正)。 */ if (ev == 1) { uint8_t st_chk[2]; ch395f_get_socket_status(sock, st_chk); - if (st_chk[0] == CH395F_TCP_ESTABLISHED) { + if (st_chk[0] == CH395F_SOCKET_OPEN && + st_chk[1] == CH395F_TCP_ESTABLISHED) { DBG_ERROR(" negative event but socket ESTABLISHED -> stale DISCONNECT!"); ev = 2; } @@ -775,6 +1061,10 @@ static void phase2_run(void) DBG_INFO("--- TC-NET-202: close/reconnect x%d ---", PHASE2_LOOP_COUNT); phase2_tc202(); + DBG_INFO("--- TC-NET-204: close A/B/C control (Trap 07) x3 modes x%d ---", + PHASE204_LOOP_COUNT); + phase2_tc204(); + DBG_INFO("--- TC-NET-203: negative test, stop PC 8081 server first ---"); phase2_tc203(); @@ -858,6 +1148,13 @@ static void phase3_run(void) { int32_t is_probe = 0; while (HAL_GetTick() - tick < 30000) { uint16_t rlen = ch395f_get_recv_len(sock); + /* ⚠ 芯片 RX 缓冲最大 2KB,本地 rx_buf 只有 sizeof(rx_buf): + * 必须夹到本地容量,否则 read_recv_buf 直接栈溢出(2026-10-11 审计修正) */ + if (rlen > (uint16_t)sizeof(rx_buf)) { + DBG_INFO(" datagram %u B > local buf %u B, truncated", + rlen, (unsigned)sizeof(rx_buf)); + rlen = (uint16_t)sizeof(rx_buf); + } if (rlen > 8) { memset(rx_buf, 0, sizeof(rx_buf)); ch395f_read_recv_buf(sock, rx_buf, rlen); @@ -1029,6 +1326,12 @@ static void phase4_run(void) { while (HAL_GetTick() - wait < PHASE4_HELLO_WAIT_MS) { ch395f_get_glob_int_status_all(); uint16_t rlen = ch395f_get_recv_len(anno_sock); + /* ⚠ 同 Phase 3:夹到本地缓冲容量,防栈溢出(2026-10-11 审计修正) */ + if (rlen > (uint16_t)sizeof(rx_buf)) { + DBG_INFO(" announce %u B > local buf %u B, truncated", + rlen, (unsigned)sizeof(rx_buf)); + rlen = (uint16_t)sizeof(rx_buf); + } if (rlen > 8) { memset(rx_buf,0,sizeof(rx_buf)); ch395f_read_recv_buf(anno_sock, rx_buf, rlen); diff --git a/test/board/net_test_task.c b/test/board/net_test_task.c index 556fcaa6..de50a61f 100644 --- a/test/board/net_test_task.c +++ b/test/board/net_test_task.c @@ -291,7 +291,8 @@ static void phase7_test_timeout(void) } /* TC 704:多 Socket 并发 TCP Client。 - * 多 Socket 模式下 Socket 1~7 才可用;本用例同时打开 Socket 0~7, + * Socket 0~7 由索引寻址 + 缓冲显式分配即可全部使用(FUN_PARA bit0 是保留位, + * 与 Socket 个数无关,见 Trap 18「定案更正」);本用例同时打开 Socket 0~7, * 各自连 PC echo server,每路独立收发 PHASE7_EXCH_SIZE 字节回显并比对, * 验证 8 个 Socket 均能独立工作。依赖 PC 端 tcp_server 支持并发连接。 */ #ifndef PHASE7_MULTI_COUNT diff --git a/test/host/README.md b/test/host/README.md index f4baeb32..f9929512 100644 --- a/test/host/README.md +++ b/test/host/README.md @@ -22,6 +22,7 @@ | `ftp_speed_test.py` | FTP 传输速度测试 | | `ftp_capture.py` | FTP 流量抓取 | | `ftp_regression.py` | FTP 回归套件 | +| `iec104_master.py` | **IEC 60870-5-104 主站客户端(B6/B7)**:连设备 :2404 跑 STARTDT → 总召(actcon/四组背景扫描/actterm)→ 经 C_SE 两段写临时压低 OC1 定值确定性触发 YX16 变位(COT=3)→ 恢复 → YK11 两段式 → C_RD 读回;**B7 持久化模式 `--b7-pre/--b7-post/--b7-mark/--b7-check`**(中间 ST-LINK 复位):YK12 两段投退→保护退出/联动、定值与压板态跨复位保持(含非默认 7.5 往返);纯标准库,输出纯 ASCII 可直接重定向归档为台架证据(判据 exit 0/1/2) | ### 时间 / 时延 @@ -41,14 +42,29 @@ | `sys_clock/run_unit_test.py` | **系统时钟换算单测**:离线编译真实 `App/util/sys_clock.c`,以可控时钟 + 阴影 RTC 桩驱动,100 断言点覆盖闰年/月末/世纪/2038 上限/星期/30 天惰性再基准化/RTC 故障降级 | | `sys_clock/sys_clock_test.c` | 测试单元本体(`#include` 生产源码 + 强符号桩;**不改生产代码**) | | `sys_clock/stub/` | 桩头文件(`main.h`/`FreeRTOS.h`/`task.h`/`dbg_log.h`),覆盖 MCU 版以在 PC 上编译 | -| `cfg_file/run_unit_test.py` | **配置引擎单测**:离线编译真实 `App/cfg/cfg_file.c`,以**内存虚拟文件系统**桩驱动,139 断言点覆盖 INI 解析/四种值类型编解码/段注册校验/load-save-poll 契约/超长守卫/故障注入 | +| `cfg_file/run_unit_test.py` | **配置引擎单测**:离线编译真实 `App/cfg/cfg_file.c`,以**内存虚拟文件系统**桩驱动,149 断言点覆盖 INI 解析/四种值类型编解码/段注册校验/load-save-poll 契约/超长守卫/故障注入 | | `cfg_file/cfg_file_test.c` | 测试单元本体(`#include` 生产源码 + `cfg_store_*` 内存 FS 后端桩;**不改生产代码**) | | `cfg_file/stub/` | 桩头文件(`dbg_log.h`),覆盖 MCU 版以在 PC 上编译 | +| `phasor/run_unit_test.py` | **相量层(L2)单测**:离线编译真实 `App/algo/phasor.c` + `sample_ring.c`(**无需任何桩**,纯算法),57 用例覆盖傅氏系数表 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 双区原子交换 | +| `phasor/phasor_test.c` | 测试单元本体(`#include` 两个生产源码 + 合成任意波形) | +| `adc_input/run_unit_test.py` | **ADC 输入层(L1)单测**:离线编译真实 `App/algo/adc_input.c`,**101** 用例给每个 Rank 注入互异码值**逐通道钉死映射**,并覆盖 DMA 挂载失败 / **乒乓半区选择(撕裂帧防线)** / 超拍丢帧 / 停摆 / HAL 回调句柄守卫 / **停摆拍陈旧帧** / 参数错等真机难复现分支 | +| `adc_input/adc_input_test.c` | 测试单元本体(`#include` 生产源码 + HAL / TPAFE 可注入桩) | +| `adc_input/stub/` | 桩头文件(`adc.h` / `tpafe5160.h`) | +| `vol_lock/run_unit_test.py` | **卷锁单测**:离线编译真实 `App/fs/vol_lock.c`,33 断言点覆盖创建 / 获取 / 释放契约(**fail-open 不变量**:创建失败仍返回 1 且零阻塞)/ 幂等 / 超时透传 | +| `vol_lock/vol_lock_test.c` | 测试单元本体(`#include` 生产源码 + CMSIS 互斥桩) | +| `vol_lock/stub/` | 桩头文件(`cmsis_os.h`) | +| `time_sync/run_unit_test.py` | **时间同步单测**:离线编译真实 `App/sync/time_sync.c`,80 断言点覆盖分片喂狗延时 / 监听退避 / 时间包重组(半包·粘连·EOF·真错·预算耗尽·tick 回绕)/ 任务主循环(**N-17 的全部逻辑贡献者**) | +| `time_sync/time_sync_test.c` | 测试单元本体(`#include` 生产源码 + 网络 / 事件 / RTOS 可脚本化桩 + `longjmp` 跳出 `for(;;)` 任务循环) | +| `time_sync/stub/` | 桩头文件(`FreeRTOS.h` / `task.h` / `cmsis_os2.h` / `net_socket.h` / `sys_clock.h` / `sys_events.h` / `dbg_log.h`) | +| `iec104/run_unit_test.py` | **IEC 60870-5-104 协议栈单测(B4)**:离线编译真实 `App/protocol/iec104/` 三模块 + 数据源(prot_set/prot_elm/phasor/sample_ring),**82** 用例覆盖 CP56 往返 / APCI 半帧·粘帧·坏帧 / STARTDT 握手 / 总召分片+RBT / COV 死区 / k=12 窗满 / w=8 触发 S / t1·t2·t3 / N(R) 倒退判故障 / 遥控·定值两段式与 PN / C_CS / 未知类型·地址错误应答 | > 详见 `docs/测试说明/看门狗单元测试说明.md`、`docs/测试说明/系统时钟单元测试说明.md`、 -> `docs/测试说明/配置文件单元测试说明.md`; +> `docs/测试说明/配置文件单元测试说明.md`、`docs/测试说明/ADC输入层单元测试说明.md`、 +> `docs/测试说明/相量层单元测试说明.md`、`docs/测试说明/卷锁单元测试说明.md`、 +> `docs/测试说明/时间同步单元测试说明.md`; > harness 用法见 [`common/README.md`](common/README.md)。 -> 已挂 pre-commit 钩子 `watchdog-unit-test`、`sys-clock-unit-test`、`cfg-file-unit-test`。 +> 已挂 pre-commit 钩子 `watchdog-unit-test`、`sys-clock-unit-test`、`cfg-file-unit-test`、 +> `phasor-unit-test`、`adc-input-unit-test`、`vol-lock-unit-test`、`time-sync-unit-test`。 ## 运行方式 @@ -69,6 +85,18 @@ # 配置引擎单测(纯逻辑,无需真机) & '.venv\Scripts\python.exe' test/host/cfg_file/run_unit_test.py + +# 相量层(L2)单测(纯逻辑,无需真机) +& '.venv\Scripts\python.exe' test/host/phasor/run_unit_test.py + +# ADC 输入层(L1)单测(纯逻辑,无需真机) +& '.venv\Scripts\python.exe' test/host/adc_input/run_unit_test.py + +# 卷锁单测(纯逻辑,无需真机) +& '.venv\Scripts\python.exe' test/host/vol_lock/run_unit_test.py + +# 时间同步单测(纯逻辑,无需真机) +& '.venv\Scripts\python.exe' test/host/time_sync/run_unit_test.py ``` > 串口端口配置在 `.serialtest/config.json`(单一配置源,当前 COM6/115200)。 @@ -77,10 +105,11 @@ - **PYTHONPATH/路径敏感**:脚本按仓库根相对路径读写,**从仓库根执行** - 本目录**被 pre-commit 门禁的 cppcheck / 代码规范排除**(`test/` 豁免); - 但 `watchdog/run_unit_test.py`、`sys_clock/run_unit_test.py`、`cfg_file/run_unit_test.py` - 自身**作为独立钩子**参与门禁(见上) + 但各模块的 `run_unit_test.py` 自身**作为独立钩子**参与门禁(见上,共 7 个) - 台架探针族(TCP 复连 / FTP 中止等)在 `MDK-ARM/*.py`,与本目录分工不同 -- `watchdog/`、`sys_clock/` 单测需要本机装有 MSVC(VS2019/2022 的 C++ 工具链);无则脚本报环境错误(退出码 2) +- 全部主机侧单测(`watchdog/`、`sys_clock/`、`cfg_file/`、`phasor/`、`adc_input/`、 + `vol_lock/`、`time_sync/`)都需要本机装有 MSVC(VS2019/2022/2026 的 C++ 工具链); + 无则脚本报环境错误(退出码 2) ## 相关文档 @@ -88,4 +117,9 @@ - `docs/测试说明/测试开发标准流程.md` — 测试分层与门禁 - `docs/测试说明/看门狗单元测试说明.md` — 主机侧单测(用例 / 平台层 / 扩展方法) - `docs/测试说明/系统时钟单元测试说明.md` — 系统时钟换算单测(用例 / 桩 / Zeller 缺陷记录) +- `docs/测试说明/配置文件单元测试说明.md` — 配置引擎单测(内存 FS 桩 / 抓出的 P0) +- `docs/测试说明/ADC输入层单元测试说明.md` — ADC 输入层单测(逐 Rank 钉死映射) +- `docs/测试说明/相量层单元测试说明.md` — 相量层单测(傅氏系数 / 双区交换) +- `docs/测试说明/卷锁单元测试说明.md` — 卷锁单测(fail-open 不变量) +- `docs/测试说明/时间同步单元测试说明.md` — 时间同步单测(N-17 逻辑贡献者) - `docs/测试说明/CH395F_Test_Guide.md`、`FTP_Test_Guide.md` diff --git a/test/host/adc_health/adc_health_test.c b/test/host/adc_health/adc_health_test.c new file mode 100644 index 00000000..724d9f90 --- /dev/null +++ b/test/host/adc_health/adc_health_test.c @@ -0,0 +1,374 @@ +/* + * 模块名称:ADC 采样有效性校验(L1-6)主机侧单元测试 + * 模块功能:在 PC 上对 App/algo/adc_health.c 做离线验证。 + * + * 被测对象:真实源文件(以 #include 原样纳入,保证测的是生产代码而非副本): + * ../../../App/algo/adc_health.c + * 本层为**纯逻辑**(仅 stdint.h),**无需任何桩**。 + * + * 为什么要主机侧单测(而不是只用板端闭环): + * 1. 静默/倒退/错误增量在健康台架上**不可复现**(复现 = 拔芯片/改驱动), + * 板端只能证明"健康时不误报",证不了"故障时必报"; + * 2. 边界(恰好 100ms 门限 / tick 回绕 / 序号倒退后再前进 / 事件只发一拍) + * 只有合成计数序列能精确构造; + * 3. 不占 MCU 的 RAM,毫秒级完成。 + * + * 覆盖点(对应 docs/算法/继电保护功能复现规划.md §3.1 L1-6): + * TC-AH-001 init + 首拍只建基线(不误报) + * TC-AH-002 正常 1 kHz 推进 2000 拍:零故障零事件(健康不误报) + * TC-AH-003 TPAFE 静默:99ms 不报 / 100ms 报 / 恢复清除电平位、锁存位留痕 + * TC-AH-004 STM32 静默:TPAFE 正常时只报 STM32 侧 + * TC-AH-005 TPAFE 丢帧:d=3 → tgap+=2、事件仅当拍(边沿语义) + * TC-AH-006 STM32 gap 计数增量 → 事件(不入锁存:超拍非故障) + * TC-AH-007 序号倒退 → SEQ_BACKWARD 锁存(前进后仍留痕) + * TC-AH-008 FRSTDATA / STM32_ERR / HW_ERR 增量 → 事件位与锁存位编号一致 + * TC-AH-009 tick 回绕:跨 0xFFFFFFFF 的正常推进不误报静默;回绕后冻结仍报 + * TC-AH-010 NULL 守卫:in=NULL 跳过;out=NULL 只更新内部状态 + * TC-AH-011 init 幂等:故障后 re-init 全清 + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-11 + */ + +#include +#include + +/* ===================== 断言 ===================== */ +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 "adc_health.c" + +/* ===================== 工具 ===================== */ + +/* 构造一份"健康"计数快照模板:两侧序号同拍号 seq,错误计数全 0 */ +static adc_health_in_t mk_in(uint32_t seq, uint32_t now_ms) { + adc_health_in_t in; + + in.tpafe_seq = seq; + in.tpafe_frst_err = 0u; + in.stm32_seq = seq; + in.stm32_errs = 0u; + in.stm32_gaps = 0u; + in.stm32_hw_errs = 0u; + in.now_ms = now_ms; + return in; +} + +/* ===================== TC ===================== */ + +/* TC-AH-001:init + 首拍只建基线 */ +static void tc_prime(void) { + adc_health_in_t in; + adc_health_status_t st; + + adc_health_init(); + in = mk_in(12345u, 1000u); /* 开机时序号已非 0 */ + adc_health_check(&in, &st); + CHECK((st.flags == 0u) && (st.events == 0u) && (st.sticky == 0u) && + (st.tpafe_gaps == 0u), + "TC-AH-001 first check only primes: no flags/events/sticky"); + /* 第二拍序号未动但仅 1ms(< 门限)→ 仍不报 */ + in.now_ms = 1001u; + adc_health_check(&in, &st); + CHECK(st.flags == 0u, "TC-AH-001 1ms without advance is not a stall"); +} + +/* TC-AH-002:正常 1 kHz 推进 2000 拍零误报 */ +static void tc_healthy(void) { + adc_health_status_t st; + adc_health_in_t in; + uint32_t seq = 0u, now = 0u; + int i, ok = 1; + + adc_health_init(); + for (i = 0; i < 2000; i++) { + in = mk_in(seq, now); + adc_health_check(&in, &st); + if ((st.flags != 0u) || (st.events != 0u) || (st.sticky != 0u)) { + ok = 0; + } + seq++; + now++; + } + CHECK(ok, "TC-AH-002 healthy 1kHz sweep 2000 beats: zero flags/events"); +} + +/* TC-AH-003:TPAFE 静默 99/100ms 门限 + 恢复留痕 */ +static void tc_tpafe_stall(void) { + adc_health_status_t st; + adc_health_in_t in; + uint32_t seq = 500u, now = 500u; + int i; + + adc_health_init(); + /* 先跑 10 拍健康节拍建立"前进中"基线 */ + for (i = 0; i < 10; i++) { + in = mk_in(seq, now); + adc_health_check(&in, &st); + seq++; now++; + } + /* 仅 TPAFE 冻结(STM32 继续走):前 100 拍(elapsed 0..99ms)不报。 + * 注意第 1 拍会消费掉 healthy 循环末尾"未消费的那次 +1", + * 故静默计时从 now=510 起算,第 i 拍 elapsed = i。 */ + for (i = 0; i < 100; i++) { + in = mk_in(seq, now); + in.stm32_seq = seq + (uint32_t)i + 1u; /* STM32 侧照常前进 */ + adc_health_check(&in, &st); + now++; + } + CHECK((st.flags & ADC_HF_TPAFE_STALL) == 0u, + "TC-AH-003 no TPAFE stall before 100ms"); + /* 第 100ms:报 */ + in = mk_in(seq, now); + in.stm32_seq = seq + 100u; + adc_health_check(&in, &st); + CHECK((st.flags & ADC_HF_TPAFE_STALL) != 0u, + "TC-AH-003 TPAFE stall at 100ms"); + CHECK((st.sticky & ADC_HF_TPAFE_STALL) != 0u, + "TC-AH-003 stall latched into sticky"); + CHECK(st.tpafe_stall_ms >= ADC_HEALTH_STALL_MS, + "TC-AH-003 stall_ms reported >= threshold"); + /* 恢复:TPAFE 序号重新前进 → 电平位清除、sticky 留痕 */ + in = mk_in(seq + 1u, now + 1u); + in.stm32_seq = seq + 101u; + adc_health_check(&in, &st); + CHECK((st.flags & ADC_HF_TPAFE_STALL) == 0u, + "TC-AH-003 stall level clears on recovery"); + CHECK((adc_health_sticky() & ADC_HF_TPAFE_STALL) != 0u, + "TC-AH-003 sticky keeps the fault trace after recovery"); +} + +/* TC-AH-004:STM32 静默(TPAFE 正常) */ +static void tc_stm32_stall(void) { + adc_health_status_t st; + adc_health_in_t in; + uint32_t seq = 100u, now = 100u; + int i; + + adc_health_init(); + for (i = 0; i < 10; i++) { + in = mk_in(seq, now); + adc_health_check(&in, &st); + seq++; now++; + } + /* STM32 冻结(TPAFE 继续走)。⚠ 第 1 拍消费 healthy 循环末尾的"未消费 +1" + * 并把静默计时起点重置为该拍 now → 第 i 拍 elapsed = i,需 101 拍才到 100ms */ + for (i = 0; i < 101; i++) { + in = mk_in(seq + (uint32_t)i + 1u, now + (uint32_t)i + 1u); + in.stm32_seq = seq; /* 冻结在旧值 */ + adc_health_check(&in, &st); + } + CHECK((st.flags & ADC_HF_STM32_STALL) != 0u, + "TC-AH-004 STM32 stall at 100ms"); + CHECK((st.flags & ADC_HF_TPAFE_STALL) == 0u, + "TC-AH-004 healthy TPAFE not falsely stalled"); +} + +/* TC-AH-005:TPAFE 丢帧计数 + 边沿语义 */ +static void tc_tpafe_gap(void) { + adc_health_status_t st; + adc_health_in_t in; + + adc_health_init(); + in = mk_in(10u, 10u); + adc_health_check(&in, &st); /* 基线 */ + in.tpafe_seq = 13u; in.stm32_seq = 11u; in.now_ms = 11u; /* TPAFE d=3 */ + adc_health_check(&in, &st); + CHECK((st.events & ADC_HE_TPAFE_GAP) != 0u, + "TC-AH-005 gap event on d>1"); + CHECK(st.tpafe_gaps == 2u, "TC-AH-005 d=3 accumulates 2 lost points"); + in.tpafe_seq = 14u; in.stm32_seq = 12u; in.now_ms = 12u; /* 恢复正常 d=1 */ + adc_health_check(&in, &st); + CHECK(st.events == 0u, "TC-AH-005 gap event is edge-only (next beat clean)"); + CHECK(st.tpafe_gaps == 2u, "TC-AH-005 gap counter is cumulative"); +} + +/* TC-AH-006:STM32 gap 计数增量 → 事件、不入锁存 */ +static void tc_stm32_gap(void) { + adc_health_status_t st; + adc_health_in_t in; + + adc_health_init(); + in = mk_in(1u, 1u); + adc_health_check(&in, &st); + in.stm32_gaps = 1u; in.tpafe_seq = 2u; in.stm32_seq = 2u; in.now_ms = 2u; + adc_health_check(&in, &st); + CHECK((st.events & ADC_HE_STM32_GAP) != 0u, + "TC-AH-006 STM32 gap increment raises event"); + CHECK((st.sticky & (ADC_HF_FRSTDATA | ADC_HF_STM32_ERR | + ADC_HF_HW_ERR)) == 0u, + "TC-AH-006 gap does NOT latch sticky (overrun is not a fault)"); +} + +/* TC-AH-007:序号倒退 → 锁存 */ +static void tc_backward(void) { + adc_health_status_t st; + adc_health_in_t in; + uint32_t s, t; + int i; + + adc_health_init(); + in = mk_in(1000u, 100u); + adc_health_check(&in, &st); + in.tpafe_seq = 995u; in.stm32_seq = 995u; in.now_ms = 101u; /* 倒退 */ + adc_health_check(&in, &st); + CHECK((st.flags & ADC_HF_SEQ_BACKWARD) != 0u, + "TC-AH-007 backward seq raises latched flag"); + CHECK((st.sticky & ADC_HF_SEQ_BACKWARD) != 0u, + "TC-AH-007 backward latched into sticky"); + /* 恢复正常推进 50 拍:SEQ_BACKWARD 不清(锁存语义) */ + s = 996u; t = 102u; + for (i = 0; i < 50; i++) { + in = mk_in(s, t); + adc_health_check(&in, &st); + s++; t++; + } + CHECK((adc_health_flags() & ADC_HF_SEQ_BACKWARD) != 0u, + "TC-AH-007 SEQ_BACKWARD persists after normal advance"); +} + +/* TC-AH-008:三类错误计数增量 → 事件位与锁存位编号一致 */ +static void tc_err_counters(void) { + adc_health_status_t st; + adc_health_in_t in; + + adc_health_init(); + in = mk_in(1u, 1u); + adc_health_check(&in, &st); + + in.tpafe_seq = 2u; in.stm32_seq = 2u; in.now_ms = 2u; + in.tpafe_frst_err = 1u; + adc_health_check(&in, &st); + CHECK(((st.events & ADC_HE_FRSTDATA) != 0u) && + ((st.sticky & ADC_HF_FRSTDATA) != 0u), + "TC-AH-008 FRSTDATA increment: event + sticky (same bit number)"); + + in.tpafe_seq = 3u; in.stm32_seq = 3u; in.now_ms = 3u; + in.stm32_errs = 1u; + adc_health_check(&in, &st); + CHECK(((st.events & ADC_HE_STM32_ERR) != 0u) && + ((st.sticky & ADC_HF_STM32_ERR) != 0u), + "TC-AH-008 STM32 err increment: event + sticky (same bit number)"); + + in.tpafe_seq = 4u; in.stm32_seq = 4u; in.now_ms = 4u; + in.stm32_hw_errs = 1u; + adc_health_check(&in, &st); + CHECK(((st.events & ADC_HE_HW_ERR) != 0u) && + ((st.sticky & ADC_HF_HW_ERR) != 0u), + "TC-AH-008 HAL err increment: event + sticky (same bit number)"); + + /* 计数不再变化 → 事件不再重复触发 */ + in.tpafe_seq = 5u; in.stm32_seq = 5u; in.now_ms = 5u; + adc_health_check(&in, &st); + CHECK(st.events == 0u, "TC-AH-008 steady error counters raise no repeat events"); +} + +/* TC-AH-009:tick 回绕安全 */ +static void tc_tick_wrap(void) { + adc_health_status_t st; + adc_health_in_t in; + uint32_t seq = 70000u, now = 0xFFFFFF00u; + int i, ok = 1; + + adc_health_init(); + /* 跨 0xFFFFFFFF 正常推进 512 拍:不得误报静默 */ + for (i = 0; i < 512; i++) { + in = mk_in(seq, now); + adc_health_check(&in, &st); + if (st.flags != 0u) { + ok = 0; + } + seq++; now++; /* uint32 自然回绕 */ + } + CHECK(ok, "TC-AH-009 tick wrap sweep: no false stall"); + /* 回绕后两侧同时冻结。⚠ 第 1 拍消费 sweep 末尾的"未消费 +1"并重置计时起点 + * → 第 i 拍 elapsed = i,需 101 拍才到 100ms */ + for (i = 0; i < 101; i++) { + in = mk_in(seq, now); + adc_health_check(&in, &st); + now++; + } + CHECK(((st.flags & ADC_HF_TPAFE_STALL) != 0u) && + ((st.flags & ADC_HF_STM32_STALL) != 0u), + "TC-AH-009 stall still detected after tick wrap"); +} + +/* TC-AH-010:NULL 守卫 */ +static void tc_null_guard(void) { + adc_health_status_t st; + adc_health_in_t in; + int i; + + adc_health_init(); + in = mk_in(1u, 1u); + adc_health_check(&in, &st); + st.flags = 0xA5A5A5A5u; + adc_health_check(NULL, &st); + CHECK(st.flags == 0xA5A5A5A5u, "TC-AH-010 in=NULL leaves out untouched"); + + /* out=NULL:内部状态照常更新(两侧冻结 101 拍 → 静默位应出现) */ + for (i = 0; i < 101; i++) { + in = mk_in(1u, (uint32_t)(2 + i)); + adc_health_check(&in, NULL); + } + CHECK((adc_health_flags() & ADC_HF_TPAFE_STALL) != 0u, + "TC-AH-010 out=NULL still updates internal state"); +} + +/* TC-AH-011:init 幂等 / 故障后全清 */ +static void tc_reinit(void) { + adc_health_status_t st; + adc_health_in_t in; + int i; + + adc_health_init(); + in = mk_in(1u, 1u); + adc_health_check(&in, &st); + for (i = 0; i < 150; i++) { /* 制造双侧静默 */ + in = mk_in(1u, (uint32_t)(2 + i)); + adc_health_check(&in, &st); + } + CHECK(st.sticky != 0u, "TC-AH-011 pre: sticky non-zero before re-init"); + adc_health_init(); + CHECK((adc_health_flags() == 0u) && (adc_health_sticky() == 0u) && + (adc_health_tpafe_gaps() == 0u), + "TC-AH-011 re-init clears flags/sticky/gaps"); + in = mk_in(9u, 9u); /* 序号跳变也无妨:首拍只建基线 */ + adc_health_check(&in, &st); + CHECK((st.flags == 0u) && (st.events == 0u), + "TC-AH-011 re-prime after re-init is clean"); +} + +int main(void) { + printf("=== ADC Health Tests ===\n"); + + printf("-- prime / healthy --\n"); + tc_prime(); + tc_healthy(); + printf("-- stall (TPAFE / STM32) --\n"); + tc_tpafe_stall(); + tc_stm32_stall(); + printf("-- gap / backward / err counters --\n"); + tc_tpafe_gap(); + tc_stm32_gap(); + tc_backward(); + tc_err_counters(); + printf("-- tick wrap / guards / re-init --\n"); + tc_tick_wrap(); + tc_null_guard(); + tc_reinit(); + + printf("\n=== ADC Health Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/adc_health/run_unit_test.py b/test/host/adc_health/run_unit_test.py new file mode 100644 index 00000000..d418e88c --- /dev/null +++ b/test/host/adc_health/run_unit_test.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""ADC 采样有效性校验(L1-6)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/adc_health/adc_health_test.c(它以 #include 纳入 + **生产源文件** App/algo/adc_health.c),在 PC 上离线跑完整的判定逻辑验证: + 首拍建基线 / 健康不误报 / 静默 100ms 门限与恢复留痕 / 丢帧边沿语义 / + 序号倒退锁存 / 三类错误计数增量 / tick 回绕 / NULL 守卫 / init 幂等, + 失败即非 0 退出。零硬件、零烧录、毫秒级。 + + 工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness + `test/host/common/unit_test_harness.py`(本文件只做声明)。 + +为什么要主机侧单测(而不是只用板端闭环): + 静默/倒退/错误增量在健康台架上不可复现(复现 = 拔芯片/改驱动)——板端只能 + 证明"健康时不误报",证不了"故障时必报";而 100ms 门限、tick 回绕、事件 + 边沿语义这些边界只有合成计数序列能精确构造。 + +本层为**纯逻辑**:无寄存器、无 RTOS、无文件/外设依赖,故**不需要任何桩目录**。 + +用法(仓库根目录): + python test/host/adc_health/run_unit_test.py + 或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/adc_health/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:docs/算法/继电保护功能复现规划.md §3.1(L1-6); + docs/测试说明/测试开发标准流程.md §4.2(L0.5 主机侧单测)。 +创建日期: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))) +# harness 位于 test/host/common/ +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 无桩目录:被测代码为纯逻辑(adc_health.c 仅依赖 stdint.h 与自身头)。 +BUILD = TestBuild( + name="ADC Health Tests", + src=os.path.join(HERE, "adc_health_test.c"), + include_dirs=[ + os.path.join(ROOT, "App", "algo"), + HERE, + ], + summary_marker="=== ADC Health Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/adc_input/adc_input_test.c b/test/host/adc_input/adc_input_test.c new file mode 100644 index 00000000..d61c091a --- /dev/null +++ b/test/host/adc_input/adc_input_test.c @@ -0,0 +1,585 @@ +/* + * 模块名称:ADC 输入层(L1)主机侧单元测试 + * 模块功能:在 PC 上对 App/algo/adc_input.c 做离线验证(8 个公开函数 + 2 个静态函数)。 + * + * 被测对象:真实源文件 ../../../App/algo/adc_input.c(以 #include 原样纳入, + * 保证测的是生产代码而非副本)。平台依赖由本文件提供的**可注入桩**实现: + * - adc.h -> HAL_ADC_Start_DMA(可注入挂载失败);DMA 完成由测试 + * 直接调用被测代码实现的 HAL_ADC_Conv[Half]CpltCallback + * 注入(dma_beat()),并可往乒乓缓冲写任意码值 + * - tpafe5160.h-> tpafe5160_snapshot(可注入 新帧/无新帧/参数错 + 帧内容) + * + * 为什么要主机侧单测(而不是只用板端套件): + * 1. 本层核心是"**通道号 <-> Rank 顺序 <-> 语义**"三者的映射正确性。板端**无信号源** + * (见 docs/硬件平台/ADC通道映射说明.md §8),无法注入已知电压去验证"读到的是 + * 哪只脚";主机侧可直接给每个 Rank 注入互不相同的码值,从而**逐通道钉死映射** + * (这是唯一能在离线端证伪"通道宏写错 -> 读到另一只脚"的手段)。 + * 2. HAL 启动失败 / 转换超时 / 参数错等分支在真机上极难稳定复现,主机侧一次钉死。 + * 3. 不占 MCU 的 RAM,毫秒级完成。 + * + * 覆盖点(对应 docs/算法/继电保护功能复现规划.md §2.1 的 L1-3 / L1-4): + * —— adc_input_init(TC-AI-001..006)—— + * TC-AI-001 poll 失败后失败计数可见(前置状态构造) + * TC-AI-002 poll 成功后 TPAFE 侧可取数(前置状态构造) + * TC-AI-003 init 返回 0(映射表自检通过) + * TC-AI-004 init 后 STM32 状态码 = NOT_RUN + * TC-AI-005 init 后失败计数归零 + * TC-AI-006 init 后 TPAFE / STM32 两侧均无有效数据(有效标志被清) + * —— adc_input_desc(TC-AI-010..01A)—— + * TC-AI-010..018 九个语义通道 -> (来源, 下标) 与权威映射表逐项一致 + * TC-AI-019/01A 越界(ADC_SEM_COUNT)与负数语义 -> NULL + * —— map_check(TC-AI-020)—— + * TC-AI-020 真实映射表自洽(反向路径结构上不可达,见文件末说明) + * —— stm32_take(TC-AI-021..078,v2.0 DMA 模型)—— + * TC-AI-021..025 取到新帧:返回 0 / 状态码 / 不计失败 / DMA 只挂一次 / seq=1 + * TC-AI-026..02B **Rank -> 通道号映射**:UB->CH14(rank9) / UC->CH3(rank4) / + * IB->CH6(rank7) / IC->CH1(rank2) / IN->CH2(rank3) / TEMP->CH5(rank6) + * TC-AI-02C stm32_take 不置 TPAFE 有效标志(两来源独立) + * TC-AI-080..089 **乒乓双缓冲**:消费侧读"刚完成的半区",即使另一半正被 DMA + * 改写也不得读到(撕裂帧防线);两拍交替半区、seq 推进; + * 超拍未消费 -> 记一次丢帧(gap),与停摆(err)分开计数 + * TC-AI-090..09A HAL 错误回调计数 + 三个回调的 `hadc == &hadc1` 句柄守卫 + * (别的句柄不得污染本层 seq / hw_errs) + * TC-AI-030..034 HAL_ADC_Start_DMA 失败:init 返回 -2 / 状态码 RC_START / 计数 +1 + * TC-AI-035..039 本拍无新帧:返回 RC_STALL / **单拍不计故障**(相位伪影)/ seq 不前进 + * TC-AI-03A..03E 连续两拍无新帧才计停摆;停摆与丢帧分开计数;恢复拍自愈 + * TC-AI-070..078 **停摆拍不得沿用上一拍旧值**(2026-10-10 补,真机实测发现的 + * 正确性缺陷;DMA 版改由"无新帧"触发同一路径):先成功一拍让 + * STM32 侧进入有效态,再注入无新帧拍 -> adc_input_raw() 必须返 -2; + * 恢复拍重新置有效。TC-AI-034 只覆盖"挂载失败即无数据", + * 照不出"成功过之后停摆 → 静默沿用旧值",故单独立一组。 + * —— adc_input_raw(TC-AI-040..04B)—— + * TC-AI-040..043 参数守卫:out=NULL / 越界 / 负数 -> -1,且不写 out + * TC-AI-044/045 无有效数据 -> -2(两来源分别) + * TC-AI-046..048 poll 快照契约:返回码透传 / 请求 8 通道 / 目的缓冲非空 + * TC-AI-049..04B TPAFE 按**下标**取数(UA=6 / UN=5 / IA=2)且补码负值原样保留 + * —— adc_input_poll(TC-AI-050..058)—— + * TC-AI-050..052 无新帧(0):返回 0 / TPAFE 无效 / STM32 侧照常扫描 + * TC-AI-053/054 参数错(-1):返回 -1 / TPAFE 无效 + * TC-AI-055 已有有效帧后再"无新帧"**不清零**(保持上次有效值) + * TC-AI-056..058 STM32 失败**不阻断** TPAFE、**不影响** poll 返回值,另由状态码暴露 + * —— adc_input_stm32_rc / _errs(TC-AI-060..066)—— + * TC-AI-060/061 init 后 = NOT_RUN / 计数 0 + * TC-AI-062 成功: rc=OK 且 errs 不变 + * TC-AI-063/064 两次失败累计 = 2,rc = 最近一次(TIMEOUT) + * TC-AI-065/066 恢复成功 rc=OK,errs 单调不回退 + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-10 + */ + +#include +#include +#include + +/* ===================== 平台桩(先纳入,覆盖真实头) ===================== */ +#include "adc.h" +#include "tpafe5160.h" +#include "adc_input.h" /* 取 ADC_INPUT_STM32_CH_NUM 等常量(桩辅助函数在被测源之前) */ + +/* ===================== HAL ADC1 桩(v2.0:DMA 模型) ===================== + * 被测代码经 HAL 只做一件事:init 时挂一次 DMA。此后"一帧完成"由 DMA 中断的 + * HAL 回调告知——回调本体由被测代码实现,测试直接调它们即可注入节拍。 + * s_start_dma_rc HAL_ADC_Start_DMA 返回值(注入挂载失败) + * s_start_dma_calls 挂载次数(正常应恒为 1:本层不反复重启 DMA) + * s_dma_buf 被测代码交下来的乒乓缓冲地址(测试据此写"采样值") + * s_dma_len 被测代码请求的搬运长度(应为 2 x 9 = 18) + */ +static HAL_StatusTypeDef s_start_dma_rc = HAL_OK; +static int s_start_dma_calls = 0; +static uint32_t *s_dma_buf = NULL; +static uint32_t s_dma_len = 0u; + +/* 被测代码把 &hadc1 透传给 HAL 桩;桩不读其字段 */ +ADC_HandleTypeDef hadc1; +/* 用于验证回调里的 `hadc == &hadc1` 守卫:别的句柄不得影响本层计数 */ +ADC_HandleTypeDef hadc_other; + +HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef *hadc, uint32_t *pData, uint32_t Length) { + (void)hadc; + s_start_dma_calls++; + s_dma_buf = pData; + s_dma_len = Length; + return s_start_dma_rc; +} + +/* ===================== TPAFE5160 快照桩 ===================== */ +static int16_t s_fake_tpafe[8]; +static int s_snapshot_rc = 0; +static int s_snapshot_calls = 0; +static uint8_t s_snapshot_count = 0u; +static int16_t *s_snapshot_dst = NULL; + +int tpafe5160_snapshot(int16_t *dst, uint8_t count) { + uint8_t i; + + s_snapshot_calls++; + s_snapshot_count = count; + s_snapshot_dst = dst; + if (s_snapshot_rc == 1) { + for (i = 0u; (i < count) && (i < 8u); i++) { + dst[i] = s_fake_tpafe[i]; + } + } + return s_snapshot_rc; +} + +/* ===================== 断言 ===================== */ +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 "adc_input.c" + +/* ===================== 工具 ===================== */ + +/* + * 函数功能:把全部桩复位到"健康"初值 + * 入口参数:无 + * 返 回 值:无 + * 限定条件:无 + * 函数说明:默认 DMA 挂载成功、TPAFE 快照无新帧。DMA 缓冲由 adc_input_init() 交下来, + * 复位时清空指针(此时不得调用 dma_fill / dma_beat)。 + */ +static void fakes_reset(void) { + uint8_t i; + + s_start_dma_rc = HAL_OK; + s_start_dma_calls = 0; + s_dma_buf = NULL; + s_dma_len = 0u; + s_snapshot_rc = 0; + s_snapshot_calls = 0; + s_snapshot_count = 0u; + s_snapshot_dst = NULL; + for (i = 0u; i < 8u; i++) { + s_fake_tpafe[i] = 0; + } +} + +/* + * 函数功能:往乒乓缓冲的指定半区写入一帧"采样值"(base + Rank 序号) + * 入口参数:half - 半区下标(0 / 1);base - 该帧首通道的码值 + * 返 回 值:无 + * 限定条件:须在 adc_input_init() 之后(缓冲地址由 Start_DMA 交下来) + * 函数说明:值按 **Rank 序号** 递增,故第 i 个 Rank = base+i;配合 + * k_stm32_rank_ch=[CH0..CH7,CH14] 可逐通道钉死映射(如 UB=base+8)。 + */ +static void dma_fill(uint8_t half, uint16_t base) { + uint16_t *p = (uint16_t *)(void *)s_dma_buf; + uint8_t i; + + for (i = 0u; i < ADC_INPUT_STM32_CH_NUM; i++) { + p[((uint32_t)half * ADC_INPUT_STM32_CH_NUM) + i] = (uint16_t)(base + i); + } +} + +/* + * 函数功能:模拟"一拍 DMA 搬运完成"(写对应半区 + 触发对应 HAL 回调) + * 入口参数:base - 本帧首通道码值 + * 返 回 值:无 + * 限定条件:须在 adc_input_init() 之后 + * 函数说明:DMA 长度 18、每拍搬 9 个 → 奇数次完成落在前半区(半传输回调)、 + * 偶数次落在后半区(传输完成回调)。半区下标由"已完成帧数"的奇偶推出, + * 与被测代码 stm32_take() 的口径互为镜像(两边独立推导,能互相证伪)。 + */ +static void dma_beat(uint16_t base) { + uint8_t half = (uint8_t)(adc_input_stm32_seq() & 1u); + + dma_fill(half, base); + if (half == 0u) { + HAL_ADC_ConvHalfCpltCallback(&hadc1); + } else { + HAL_ADC_ConvCpltCallback(&hadc1); + } +} + +/* ===================== TC-AI-001..006:init ===================== */ + +static void tc_init(void) { + int32_t v = -999; + + fakes_reset(); + /* 前置:制造"STM32 失败一次 + TPAFE 有效一帧"的状态,再验 init 是否清干净 */ + s_snapshot_rc = 1; + s_fake_tpafe[6] = 4321; + (void)adc_input_poll(); /* 单拍无新帧 = 相位伪影,不计故障 */ + (void)adc_input_poll(); /* 连续第二拍无新帧 = 真停摆 */ + + CHECK(adc_input_stm32_errs() == 1u, "TC-AI-001 连续两拍无新帧 -> 停摆计数 = 1"); + CHECK((adc_input_raw(ADC_SEM_UA, &v) == 0) && (v == 4321), + "TC-AI-002 poll 后 UA 可取数且值正确"); + + CHECK(adc_input_init() == 0, "TC-AI-003 adc_input_init 返回 0(映射自检通过)"); + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_NOT_RUN, + "TC-AI-004 init 后 STM32 状态码 = NOT_RUN"); + CHECK(s_start_dma_calls == 1, "TC-AI-003B init 挂载 DMA 恰好一次"); + CHECK(s_dma_len == (2u * ADC_INPUT_STM32_CH_NUM), "TC-AI-003C DMA 长度 = 2 x 9(乒乓双缓冲)"); + CHECK(s_dma_buf != NULL, "TC-AI-003D DMA 目的缓冲地址已交下来"); + CHECK(adc_input_stm32_seq() == 0u, "TC-AI-005B init 后 DMA 完成计数 = 0"); + CHECK(adc_input_stm32_errs() == 0u, "TC-AI-005 init 后 STM32 失败计数归零"); + CHECK((adc_input_raw(ADC_SEM_UA, &v) == -2) && (adc_input_raw(ADC_SEM_UB, &v) == -2), + "TC-AI-006 init 后 TPAFE / STM32 两侧均无有效数据"); +} + +/* ===================== TC-AI-010..01A:desc 映射表 ===================== */ + +static void tc_desc(void) { + const adc_channel_desc_t *d; + + d = adc_input_desc(ADC_SEM_UA); + CHECK((d != NULL) && (d->source == ADC_SRC_TPAFE) && (d->index == 6u), + "TC-AI-010 UA -> TPAFE buf[6]"); + d = adc_input_desc(ADC_SEM_UB); + CHECK((d != NULL) && (d->source == ADC_SRC_STM32) && (d->index == 14u), + "TC-AI-011 UB -> STM32 CH14"); + d = adc_input_desc(ADC_SEM_UC); + CHECK((d != NULL) && (d->source == ADC_SRC_STM32) && (d->index == 3u), + "TC-AI-012 UC -> STM32 CH3"); + d = adc_input_desc(ADC_SEM_UN); + CHECK((d != NULL) && (d->source == ADC_SRC_TPAFE) && (d->index == 5u), + "TC-AI-013 UN -> TPAFE buf[5]"); + d = adc_input_desc(ADC_SEM_IA); + CHECK((d != NULL) && (d->source == ADC_SRC_TPAFE) && (d->index == 2u), + "TC-AI-014 IA -> TPAFE buf[2]"); + d = adc_input_desc(ADC_SEM_IB); + CHECK((d != NULL) && (d->source == ADC_SRC_STM32) && (d->index == 6u), + "TC-AI-015 IB -> STM32 CH6"); + d = adc_input_desc(ADC_SEM_IC); + CHECK((d != NULL) && (d->source == ADC_SRC_STM32) && (d->index == 1u), + "TC-AI-016 IC -> STM32 CH1"); + d = adc_input_desc(ADC_SEM_IN); + CHECK((d != NULL) && (d->source == ADC_SRC_STM32) && (d->index == 2u), + "TC-AI-017 IN -> STM32 CH2"); + d = adc_input_desc(ADC_SEM_TEMP); + CHECK((d != NULL) && (d->source == ADC_SRC_STM32) && (d->index == 5u), + "TC-AI-018 TEMP -> STM32 CH5"); + + CHECK(adc_input_desc(ADC_SEM_COUNT) == NULL, "TC-AI-019 越界(ADC_SEM_COUNT) -> NULL"); + CHECK(adc_input_desc((adc_semantic_t)(-1)) == NULL, "TC-AI-01A 负数语义 -> NULL"); +} + +/* ===================== TC-AI-020:map_check ===================== */ + +static void tc_map_check(void) { + /* 真实映射表自洽:TPAFE 下标 < 8 且每个 STM32 通道号都出现在扫描 Rank 内。 + * 反向路径(表不自洽 -> -1)需在运行时改表,而 k_adc_map / k_stm32_rank_ch 均为 + * 编译期 static const、无运行时输入可使其不自洽 -> 结构上不可达(见文件末说明)。 */ + CHECK(map_check() == 0, "TC-AI-020 映射表自洽(TPAFE 下标与 STM32 Rank 均在范围内)"); +} + +/* ===================== TC-AI-021..02C:stm32_scan 成功路径 ===================== */ + +static void tc_take_ok(void) { + int32_t v = 0; + + fakes_reset(); + (void)adc_input_init(); + dma_beat(100); /* 前半区一帧:Rank i -> 100+i */ + + CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-AI-021 stm32_take 取到新帧 -> 0"); + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-AI-022 状态码 = RC_OK"); + CHECK(adc_input_stm32_errs() == 0u, "TC-AI-023 成功不计入失败"); + CHECK(s_start_dma_calls == 1, "TC-AI-024 DMA 只在 init 挂一次(每拍不重启)"); + CHECK(adc_input_stm32_seq() == 1u, "TC-AI-025 DMA 完成计数 = 1"); + + /* Rank -> 通道号映射:第 i 个 Rank = 100+i。 + * k_stm32_rank_ch = [CH0..CH7, CH14],故 CH3->103 / CH5->105 / CH6->106 / CH14->108。 */ + CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), "TC-AI-026 UB 取到 CH14(rank9)=108"); + CHECK((adc_input_raw(ADC_SEM_UC, &v) == 0) && (v == 103), "TC-AI-027 UC 取到 CH3(rank4)=103"); + CHECK((adc_input_raw(ADC_SEM_IB, &v) == 0) && (v == 106), "TC-AI-028 IB 取到 CH6(rank7)=106"); + CHECK((adc_input_raw(ADC_SEM_IC, &v) == 0) && (v == 101), "TC-AI-029 IC 取到 CH1(rank2)=101"); + CHECK((adc_input_raw(ADC_SEM_IN, &v) == 0) && (v == 102), "TC-AI-02A IN 取到 CH2(rank3)=102"); + CHECK((adc_input_raw(ADC_SEM_TEMP, &v) == 0) && (v == 105), "TC-AI-02B TEMP 取到 CH5(rank6)=105"); + + CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-AI-02C stm32_take 不置 TPAFE 有效标志"); +} + +/* ============ TC-AI-080..085:乒乓双缓冲(读刚完成半区,不读正在写的半区) ============ + * 这是 DMA 方案最关键的一条:消费侧必须读"上一拍刚完成的那一半",而 DMA 此刻正在 + * 写另一半。若读错半区,取到的是**正在被改写**的数据 → 半新半旧的撕裂帧。 */ + +static void tc_dma_pingpong(void) { + int32_t v = 0; + + fakes_reset(); + (void)adc_input_init(); + + dma_beat(100); /* 第 1 帧 -> 前半区(半传输回调) */ + dma_fill(1u, 900u); /* 模拟 DMA 正在写后半区(值 900 系) */ + CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-AI-080 第一拍取帧成功"); + CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), + "TC-AI-081 读的是刚完成的前半区(108),不是正在写的后半区"); + + dma_beat(200); /* 第 2 帧 -> 后半区(传输完成回调) */ + dma_fill(0u, 800u); /* 模拟 DMA 正在写前半区(值 800 系) */ + CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-AI-082 第二拍取帧成功"); + CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 208), + "TC-AI-083 第二拍读到后半区(208)"); + CHECK(adc_input_stm32_seq() == 2u, "TC-AI-084 DMA 完成计数 = 2"); + + /* 半区奇偶口径:seq=1 -> 前半区,seq=2 -> 后半区(与 dma_beat 独立推导,互相证伪) */ + CHECK(((adc_input_raw(ADC_SEM_UC, &v) == 0) && (v == 203)), "TC-AI-085 UC 同帧内一致(203)"); + + /* 消费者超拍:两拍都没取 -> 中间那帧已被乒乓缓冲覆盖 -> 记一次丢帧 */ + dma_beat(300); + dma_beat(400); + CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-AI-086 超拍后仍取到最新帧"); + CHECK(adc_input_stm32_gaps() == 1u, "TC-AI-087 seq 前进 2 -> 记一次丢帧"); + CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 408), "TC-AI-088 取到的是最新帧(408)"); + CHECK(adc_input_stm32_errs() == 0u, "TC-AI-089 丢帧不计入停摆(两类故障分开)"); +} + +/* ============ TC-AI-090..093:HAL 错误回调与句柄守卫 ============ */ + +static void tc_hw_errs(void) { + fakes_reset(); + (void)adc_input_init(); + + HAL_ADC_ErrorCallback(&hadc1); + HAL_ADC_ErrorCallback(&hadc1); + CHECK(adc_input_stm32_hw_errs() == 2u, "TC-AI-090 错误回调累计 = 2"); + + HAL_ADC_ErrorCallback(&hadc_other); + CHECK(adc_input_stm32_hw_errs() == 2u, "TC-AI-091 别的 ADC 句柄不得计入(hadc 守卫)"); + + HAL_ADC_ConvHalfCpltCallback(&hadc_other); + HAL_ADC_ConvCpltCallback(&hadc_other); + CHECK(adc_input_stm32_seq() == 0u, "TC-AI-092 别的句柄的完成回调不得推进 seq"); + + HAL_ADC_ErrorCallback(&hadc1); + CHECK(adc_input_stm32_hw_errs() == 3u, "TC-AI-093 hw_errs 与 errs 分开计数、互不干扰"); + CHECK(adc_input_stm32_errs() == 0u, "TC-AI-09A hw_errs 不污染 errs"); +} + +/* ===================== TC-AI-030..034:DMA 挂载失败 ===================== */ + +static void tc_init_dma_fail(void) { + int32_t v = 0; + + fakes_reset(); + s_start_dma_rc = HAL_ERROR; + + CHECK(adc_input_init() == -2, "TC-AI-030 HAL_ADC_Start_DMA 失败 -> init 返回 -2"); + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_START, "TC-AI-031 状态码 = RC_START"); + CHECK(adc_input_stm32_errs() == 1u, "TC-AI-032 失败计数 +1"); + CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-AI-034 挂载失败后 STM32 侧无有效数据"); +} + +/* ===================== TC-AI-035..03A:本拍无新帧(停摆) ===================== */ + +static void tc_take_stall(void) { + int32_t v = 0; + + fakes_reset(); + (void)adc_input_init(); + /* 没有任何 DMA 完成回调 = TIM3 未启动 / ADC 停摆 / DMA 出错 */ + + CHECK(stm32_take() == ADC_INPUT_RC_STALL, "TC-AI-035 无新帧 -> RC_STALL(-2)"); + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-AI-036 状态码 = RC_STALL"); + CHECK(adc_input_stm32_errs() == 0u, "TC-AI-037 单拍无新帧不计故障(相位伪影)"); + CHECK(adc_input_stm32_seq() == 0u, "TC-AI-038 停摆期间 seq 不前进"); + CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-AI-039 停摆拍 STM32 侧无有效数据"); + + (void)stm32_take(); /* 连续第二拍无新帧 = 真停摆 */ + CHECK(adc_input_stm32_errs() == 1u, "TC-AI-03A 连续两拍无新帧 -> 停摆计数 = 1"); + (void)stm32_take(); /* 第三拍仍无新帧 */ + CHECK(adc_input_stm32_errs() == 2u, "TC-AI-03B 停摆持续 -> 每拍累计 = 2"); + CHECK(adc_input_stm32_gaps() == 0u, "TC-AI-03C 停摆不计入丢帧(两类故障分开)"); + + dma_beat(100); /* 恢复 */ + CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-AI-03D 恢复后取帧成功"); + CHECK(adc_input_stm32_gaps() == 0u, "TC-AI-03E 恢复拍前进 1 -> 不算丢帧"); +} + +/* ============ TC-AI-070..078:停摆拍不得沿用上一拍旧值(陈旧帧) ============ + * 背景(2026-10-10 真机实测发现):本层只有一份 s_stm32_raw[],而 s_stm32_valid + * 此前只在成功时置 1、失败时**从不清零** → 消费侧照样取数、照样压入采样环, + * s_frames_skip 抓不到。轮询版的触发路径是"超时拍半新半旧",DMA 版(v2.0)改成 + * "本拍无新帧仍沿用上一拍"——**同一条缺陷、同一个防线**,故本组用例保留。 + * TC-AI-034 是"挂载失败后本就无数据"(valid 一直是 0),照不出这条路径—— + * 必须先成功一拍。 */ + +static void tc_take_stale(void) { + int32_t v = 0; + + /* --- 停摆支路:成功拍 -> 无新帧拍 -> 恢复拍 --- */ + fakes_reset(); + (void)adc_input_init(); + dma_beat(100); + CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-AI-070 前置:首拍成功"); + CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), "TC-AI-071 前置:UB 取到 108(有效态)"); + + /* 第二拍没有任何 DMA 完成(TIM3 停 / ADC 停摆 / DMA 出错) */ + CHECK(stm32_take() == ADC_INPUT_RC_STALL, "TC-AI-072 第二拍无新帧"); + CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-AI-073 停摆拍不得沿用上一拍值 -> -2"); + CHECK(adc_input_raw(ADC_SEM_UC, &v) == -2, "TC-AI-074 停摆拍连其它 STM32 通道也不得出数"); + CHECK(adc_input_raw(ADC_SEM_TEMP, &v) == -2, "TC-AI-074B 同上(TEMP 亦 -2)"); + + dma_beat(200); + CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-AI-075 第三拍恢复成功"); + CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 208), "TC-AI-076 恢复拍重新可取数(自愈)"); + + /* --- 挂载失败支路:同样不得沿用(init 失败后整层无有效数据) --- */ + fakes_reset(); + s_start_dma_rc = HAL_ERROR; + (void)adc_input_init(); + CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-AI-078 挂载失败后不得取到任何值 -> -2"); +} + +/* ===================== TC-AI-040..045:raw 参数守卫与无数据 ===================== */ + +static void tc_raw_guards(void) { + int32_t v = 0; + + fakes_reset(); + (void)adc_input_init(); + + CHECK(adc_input_raw(ADC_SEM_UA, NULL) == -1, "TC-AI-040 out=NULL -> -1"); + CHECK(adc_input_raw(ADC_SEM_COUNT, &v) == -1, "TC-AI-041 ch 越界 -> -1"); + CHECK(adc_input_raw((adc_semantic_t)(-1), &v) == -1, "TC-AI-042 ch 负数 -> -1"); + CHECK(v == 0, "TC-AI-043 参数错时不写 out"); + CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-AI-044 TPAFE 无有效帧 -> -2"); + CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-AI-045 STM32 无有效扫描 -> -2"); +} + +/* ===================== TC-AI-046..04B:raw TPAFE 取数与补码 ===================== */ + +static void tc_raw_tpafe(void) { + int32_t v = 0; + + fakes_reset(); + (void)adc_input_init(); + /* 注入 8 路 TPAFE 码值(互不相同),UA 应取 buf[6] / UN 取 buf[5] / IA 取 buf[2] */ + s_fake_tpafe[0] = 1000; + s_fake_tpafe[1] = 1001; + s_fake_tpafe[2] = -1234; + s_fake_tpafe[3] = 1003; + s_fake_tpafe[4] = 1004; + s_fake_tpafe[5] = 2005; + s_fake_tpafe[6] = 3006; + s_fake_tpafe[7] = 1007; + s_snapshot_rc = 1; + + CHECK(adc_input_poll() == 1, "TC-AI-046 poll 透传 TPAFE 快照码 1"); + CHECK(s_snapshot_count == (uint8_t)ADC_INPUT_TPAFE_CH_NUM, "TC-AI-047 快照请求 8 通道"); + CHECK(s_snapshot_dst != NULL, "TC-AI-048 快照目的缓冲非空"); + CHECK((adc_input_raw(ADC_SEM_UA, &v) == 0) && (v == 3006), "TC-AI-049 UA -> buf[6]=3006"); + CHECK((adc_input_raw(ADC_SEM_UN, &v) == 0) && (v == 2005), "TC-AI-04A UN -> buf[5]=2005"); + CHECK((adc_input_raw(ADC_SEM_IA, &v) == 0) && (v == -1234), + "TC-AI-04B IA -> buf[2]=-1234(补码负值原样保留)"); +} + +/* ===================== TC-AI-050..058:poll 契约 ===================== */ + +static void tc_poll(void) { + int32_t v = 0; + + /* --- 无新帧(0):返回 0,TPAFE 无效,STM32 侧照常扫描 --- */ + fakes_reset(); + (void)adc_input_init(); + s_snapshot_rc = 0; + dma_beat(100); + CHECK(adc_input_poll() == 0, "TC-AI-050 快照无新帧 -> poll 返回 0"); + CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-AI-051 无新帧时 TPAFE 侧无效"); + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-AI-052 同拍 STM32 取帧照常执行"); + + /* --- 参数错(-1):返回 -1,TPAFE 无效 --- */ + fakes_reset(); + (void)adc_input_init(); + s_snapshot_rc = -1; + CHECK(adc_input_poll() == -1, "TC-AI-053 快照参数错 -> poll 返回 -1"); + CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-AI-054 参数错时 TPAFE 侧无效"); + + /* --- 已有有效帧后再"无新帧":保持上次有效值(不清零) --- */ + fakes_reset(); + (void)adc_input_init(); + s_fake_tpafe[6] = 777; + s_snapshot_rc = 1; + (void)adc_input_poll(); + s_snapshot_rc = 0; + (void)adc_input_poll(); + CHECK((adc_input_raw(ADC_SEM_UA, &v) == 0) && (v == 777), + "TC-AI-055 已有有效帧后无新帧不清零"); + + /* --- STM32 失败不阻断 TPAFE、不影响 poll 返回值 --- */ + fakes_reset(); + (void)adc_input_init(); + s_fake_tpafe[2] = 55; + s_snapshot_rc = 1; + /* 不注入任何 DMA 完成 = STM32 侧停摆 */ + CHECK(adc_input_poll() == 1, "TC-AI-056 STM32 停摆不影响 poll 返回值(仍为 1)"); + CHECK((adc_input_raw(ADC_SEM_IA, &v) == 0) && (v == 55), + "TC-AI-057 STM32 停摆不影响 TPAFE 取数"); + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-AI-058 STM32 停摆另由状态码暴露"); +} + +/* ===================== TC-AI-060..066:rc / errs 观测 ===================== */ + +static void tc_rc_errs(void) { + fakes_reset(); + (void)adc_input_init(); + + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_NOT_RUN, "TC-AI-060 init 后未扫描 = NOT_RUN(-3)"); + CHECK(adc_input_stm32_errs() == 0u, "TC-AI-061 init 后失败计数 = 0"); + + dma_beat(100); + (void)stm32_take(); /* 成功 */ + CHECK((adc_input_stm32_rc() == ADC_INPUT_RC_OK) && (adc_input_stm32_errs() == 0u), + "TC-AI-062 成功: rc=OK 且 errs 不变"); + + (void)stm32_take(); /* 停摆 1(单拍 = 相位伪影,不计) */ + (void)stm32_take(); /* 停摆 2 -> 计 1 */ + CHECK(adc_input_stm32_errs() == 1u, "TC-AI-063 连续两拍停摆累计 = 1"); + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-AI-064 rc = 最近一次(STALL)"); + + dma_beat(200); + (void)stm32_take(); /* 恢复成功 */ + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-AI-065 恢复成功后 rc=OK"); + CHECK(adc_input_stm32_errs() == 1u, "TC-AI-066 errs 单调递增(不因成功回落)"); + CHECK(adc_input_stm32_seq() == 2u, "TC-AI-067 seq 只由 DMA 完成回调推进"); +} + +/* ===================== main ===================== */ + +int main(void) { + printf("=== ADC Input Tests ===\n"); + + printf("-- init / reset --\n"); + tc_init(); + printf("-- semantic map desc --\n"); + tc_desc(); + printf("-- map self-check --\n"); + tc_map_check(); + printf("-- stm32 take: success + rank/channel mapping --\n"); + tc_take_ok(); + printf("-- stm32 dma: ping-pong double buffer --\n"); + tc_dma_pingpong(); + printf("-- stm32 dma: HAL error callback + handle guard --\n"); + tc_hw_errs(); + printf("-- stm32 dma: Start_DMA fail --\n"); + tc_init_dma_fail(); + printf("-- stm32 take: no new frame (stall) --\n"); + tc_take_stall(); + printf("-- stm32 take: stale frame on stall beat --\n"); + tc_take_stale(); + printf("-- raw: guards / no-data --\n"); + tc_raw_guards(); + printf("-- raw: tpafe index + two's complement --\n"); + tc_raw_tpafe(); + printf("-- poll contract --\n"); + tc_poll(); + printf("-- rc / errs observability --\n"); + tc_rc_errs(); + + printf("\n=== ADC Input Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/adc_input/run_unit_test.py b/test/host/adc_input/run_unit_test.py new file mode 100644 index 00000000..b55e4aa7 --- /dev/null +++ b/test/host/adc_input/run_unit_test.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""ADC 输入层(L1,adc_input)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/adc_input/adc_input_test.c(它以 #include 纳入 + **生产源文件** App/algo/adc_input.c),在 PC 上离线跑语义映射表 / STM32 ADC1 + DMA 乒乓帧消费(含半区选择)/ TPAFE 快照消费 / 取数守卫 / poll 契约 / 四类故障 + 计数(seq / gap / err / hw),失败即非 0 退出。 + 零硬件、零烧录、毫秒级。 + + 工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness + `test/host/common/unit_test_harness.py`(本文件只做声明)。 + +为什么要主机侧单测(而不是只用板端套件): + 本层核心是"**通道号 <-> Rank 顺序 <-> 语义**"三者映射的正确性。板端无信号源 + (docs/硬件平台/ADC通道映射说明.md §8),无法注入已知电压验证"读到的是哪只脚"; + 主机侧给每个 Rank 注入互不相同的码值,即可**逐通道钉死映射**,并一次钉死 + HAL 启动失败 / 转换超时 / 参数错等真机难复现的分支。 + +用法(仓库根目录): + python test/host/adc_input/run_unit_test.py + 或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/adc_input/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:docs/算法/继电保护功能复现规划.md §2.1(L1-3 / L1-4); +创建日期:2026-10-10 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +# harness 位于 test/host/common/ +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 顺序敏感:stub 目录必须**先于**生产目录,才能覆盖 adc.h / tpafe5160.h +# (真实头会经 main.h 拖入整套 HAL,主机侧无法编译)。adc_input.h 用真实头 +# (接口契约原汁);HAL_ADC_* 与 tpafe5160_snapshot 由测试单元给强符号桩。 +BUILD = TestBuild( + name="ADC Input Tests", + src=os.path.join(HERE, "adc_input_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "App", "algo"), + HERE, + ], + summary_marker="=== ADC Input Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/adc_input/stub/adc.h b/test/host/adc_input/stub/adc.h new file mode 100644 index 00000000..7600efe7 --- /dev/null +++ b/test/host/adc_input/stub/adc.h @@ -0,0 +1,64 @@ +/* + * 主机侧单测桩:adc.h + * 模块功能:为 App/algo/adc_input.c 提供主机可编译的 STM32 ADC1 句柄与 HAL 接口 + * 声明(真实 Inc/adc.h 会经 main.h 拖入整套 HAL,主机侧无法编译)。 + * + * ⚠ 仅供 test/host/adc_input/ 使用,不参与 MCU 构建。 + * + * 保真点:F4 HAL 的 ADC_CHANNEL_x 宏值**即通道号 x**(0..15),本桩按此定义, + * 故被测代码里 `(uint8_t)ADC_CHANNEL_14` 得到 14 —— 与真机一致。 + * 另声明 1 个 HAL_ADC_Start_DMA 接口(由测试单元给强符号桩,可注入挂载失败)与 + * 3 个 HAL 回调(**由被测代码 adc_input.c 实现**,测试直接调用它们模拟 DMA 中断)。 + * + * 创建日期:2026-10-10 + */ + +#ifndef ADC_H +#define ADC_H + +#include + +/* HAL 状态码(与 stm32f4xx_hal_def.h 同值,仅取被测代码用到的子集) */ +typedef enum { + HAL_OK = 0x00, + HAL_ERROR = 0x01, + HAL_BUSY = 0x02, + HAL_TIMEOUT = 0x03 +} HAL_StatusTypeDef; + +/* ADC 句柄:被测代码只把 &hadc1 透传给 HAL 接口,从不读其字段,故为不透明占位 */ +typedef struct { + uint32_t reserved; +} ADC_HandleTypeDef; + +extern ADC_HandleTypeDef hadc1; + +/* ADC 通道宏(F4:宏值 = 通道号) */ +#define ADC_CHANNEL_0 (0x00000000U) +#define ADC_CHANNEL_1 (0x00000001U) +#define ADC_CHANNEL_2 (0x00000002U) +#define ADC_CHANNEL_3 (0x00000003U) +#define ADC_CHANNEL_4 (0x00000004U) +#define ADC_CHANNEL_5 (0x00000005U) +#define ADC_CHANNEL_6 (0x00000006U) +#define ADC_CHANNEL_7 (0x00000007U) +#define ADC_CHANNEL_8 (0x00000008U) +#define ADC_CHANNEL_9 (0x00000009U) +#define ADC_CHANNEL_10 (0x0000000AU) +#define ADC_CHANNEL_11 (0x0000000BU) +#define ADC_CHANNEL_12 (0x0000000CU) +#define ADC_CHANNEL_13 (0x0000000DU) +#define ADC_CHANNEL_14 (0x0000000EU) +#define ADC_CHANNEL_15 (0x0000000FU) + +/* v2.0:STM32 侧改 TIM3 TRGO 触发 + DMA 双缓冲,本层只调 Start_DMA 一次; + * 逐通道 Start / PollForConversion / GetValue / Stop 已从被测代码中消失,桩同步删除 */ +HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef *hadc, uint32_t *pData, uint32_t Length); + +/* 三个 HAL 弱回调:真机上由 DMA 中断上下文调用,这里由**被测代码实现**、 + * 由测试单元直接调用以模拟"一帧搬运完成" */ +void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc); +void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc); +void HAL_ADC_ErrorCallback(ADC_HandleTypeDef *hadc); + +#endif /* ADC_H */ diff --git a/test/host/adc_input/stub/tpafe5160.h b/test/host/adc_input/stub/tpafe5160.h new file mode 100644 index 00000000..69a9516f --- /dev/null +++ b/test/host/adc_input/stub/tpafe5160.h @@ -0,0 +1,28 @@ +/* + * 主机侧单测桩:tpafe5160.h + * 模块功能:为 App/algo/adc_input.c 提供主机可编译的 TPAFE5160 快照接口声明 + * (真实头会经 main.h 拖入 HAL 与 GPIO 宏,主机侧无法编译)。 + * + * ⚠ 仅供 test/host/adc_input/ 使用,不参与 MCU 构建。 + * + * 被测代码只消费 tpafe5160_snapshot()(E-58 原子快照),故桩只保留该声明; + * 其返回值语义(1=新帧 / 0=无新帧 / -1=参数错)与真实驱动一致,由测试单元 + * 给强符号桩并按用例注入。 + * + * 创建日期:2026-10-10 + */ + +#ifndef TPAFE5160_H +#define TPAFE5160_H + +#include + +/* + * 函数功能:原子快照最新一帧(消费者取数的唯一正确姿势) + * 入口参数:dst - 输出缓冲区 int16_t* 不为 NULL + * count - 拷贝通道数 uint8_t 1 - 8 + * 返 回 值:1 - 取到新帧,0 - 无新帧,-1 - 参数无效 + */ +int tpafe5160_snapshot(int16_t *dst, uint8_t count); + +#endif /* TPAFE5160_H */ diff --git a/test/host/ch395f_reuse_host.py b/test/host/ch395f_reuse_host.py new file mode 100644 index 00000000..5dae2f31 --- /dev/null +++ b/test/host/ch395f_reuse_host.py @@ -0,0 +1,92 @@ +# N-30 芯片级复用时序台架:主机端(与 test/board/ch395f_reuse_bench.c 共用变体表) +# 用法: python ch395f_reuse_host.py [--host IP] [--cycles N] +# 纯 TCP,无 FTP。按 idx 轮转连接 5000+idx 端口,记录收发结果与时刻。 +import argparse +import socket +import sys +import time + +# {idx: (tx_len, close_mode)} 与固件表一致 +VARS = { + 0: (100, 0), # 0ms control + 1: (100, 0), # 300ms control + 2: (2000, 0), # 2KB control + 3: (100, 0), # 0ms + ghost-wait fix + 4: (100, 0), # 300ms + ghost-wait fix + 5: (2000, 0), # 2KB + ghost-wait fix +} + + +def ts(): + return time.strftime("%H:%M:%S") + ".%03d" % (int(time.time() * 1000) % 1000) + + +def run_round(host, idx, tx_len, close_mode): + port = 5000 + idx + # 等端口监听(芯片还没 open 时连接被拒,重试) + s = None + t0 = time.time() + while time.time() - t0 < 12.0: + try: + s = socket.create_connection((host, port), timeout=5) + break + except OSError: + time.sleep(0.2) + if s is None: + print("%s r%u CONNECT timeout" % (ts(), idx)) + return False + + ok = True + try: + s.settimeout(5) + got = 0 + while got < tx_len: + chunk = s.recv(min(1024, tx_len - got)) + if not chunk: + break + got += len(chunk) + note = "" + if got != tx_len: + note = " <<<< SWALLOWED (want %d)" % tx_len + ok = False + if close_mode == 1: + # 等芯片 FIN(mode1 芯片先关) + try: + fin = s.recv(1) + if fin: + note += " unexpected data after tx: %r" % fin + except socket.timeout: + note += " <<<< no FIN from chip (mode1)" + ok = False + print("%s r%u connected got=%d/%d%s" % (ts(), idx, got, tx_len, note)) + except socket.timeout: + print("%s r%u recv timeout <<<< STALL" % (ts(), idx)) + ok = False + except OSError as e: + print("%s r%u sock error %r" % (ts(), idx, e)) + ok = False + finally: + s.close() # 主机关闭(mode0/2 主机先关) + return ok + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--host", default="192.168.1.100") + ap.add_argument("--cycles", type=int, default=3) + args = ap.parse_args() + + fails = [] + for c in range(args.cycles): + for idx in sorted(VARS): + tx_len, mode = VARS[idx] + print("%s === cycle%u r%u (tx=%d mode=%d) ===" % (ts(), c, idx, tx_len, mode)) + if not run_round(args.host, idx, tx_len, mode): + fails.append((c, idx)) + time.sleep(0.3) + print("RESULT: %d failures %s" % (len(fails), fails)) + return 1 if fails else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/host/common/unit_test_harness.py b/test/host/common/unit_test_harness.py index 3d520fd2..6519baac 100644 --- a/test/host/common/unit_test_harness.py +++ b/test/host/common/unit_test_harness.py @@ -36,7 +36,7 @@ summary_marker="=== Task Watchdog Tests:", ))) -设计依据:docs/待处理内容.md T-3;docs/测试说明/测试开发标准流程.md §4.2 +设计依据:docs/任务规划/登记簿.md T-3;docs/测试说明/测试开发标准流程.md §4.2 创建日期:2026-10-01 """ diff --git a/test/host/ftp_auth/ftp_auth_test.c b/test/host/ftp_auth/ftp_auth_test.c new file mode 100644 index 00000000..4938e2ac --- /dev/null +++ b/test/host/ftp_auth/ftp_auth_test.c @@ -0,0 +1,203 @@ +/* + * 模块名称:FTP 鉴权状态机(lftpd_auth)主机侧单元测试 + * 模块功能:在 PC 上对 Drivers/BSP/NET/lftpd/lftpd_auth.c 做离线验证 + * (用户名匹配 / 口令校验 / 失败计数 / 限时锁定 / tick 回绕)。 + * + * 被测对象:真实源文件 lftpd_auth.c + 其依赖 sha256.c(均以 #include 原样 + * 纳入,保证测的是生产代码而非副本;两模块零平台依赖,无需桩—— + * tick 由参数注入正是本模块的纯函数设计意图)。 + * + * 为什么要主机侧单测(而不是只用台架): + * 锁定 60 s / 解锁 / 成功清零 / tick 32 位回绕是"防口令爆破"契约的核心, + * 板端每验一条要真等 60 s 且回绕要跑满 49.7 天;主机侧注入合成 tick + * 毫秒级穷举。用户名侧信道路径(错用户名也走哈希)同属纯逻辑。 + * + * 覆盖点: + * TC-FA-001 user_match:相等 / 大小写不敏感 / 不等 / NULL + * TC-FA-002 正确用户名+正确口令 → OK,计数 0 + * TC-FA-003 错口令 → FAIL,计数 1 + * TC-FA-004 错用户名+对口令 → FAIL(哈希路径已走,时间拉平) + * TC-FA-005 连续 5 次失败 → 第 6 次(锁定期内)LOCKED,对口令也拒 + * TC-FA-006 锁定期满(+60000 tick)→ 对口令 OK,计数清零 + * TC-FA-007 成功清零计数:3 败 + 1 成 → 再 4 败不触发锁定(第 5 败才锁) + * TC-FA-008 tick 32 位回绕:锁定期内回绕点仍 LOCKED,期满回绕点放行 + * TC-FA-009 配置 pwd_hash 非法 hex → 恒 FAIL(fail-closed) + * TC-FA-010 空口令 / NULL 口令 → FAIL;NULL creds/state → FAIL + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-11 + */ + +#include +#include + +/* 生产源文件原样纳入(顺序敏感:先 sha256 后 lftpd_auth) */ +#include "sha256.c" +#include "lftpd_auth.c" + +/* ===================== 断言框架 ===================== */ + +static int s_total; +static int s_passed; +static int s_failed; + +#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) + +/* 与 ftp_cfg.h FTP_CFG_DEF_HASH 同源钉值(SHA256("adminadmin")) */ +#define DEF_HASH "d82494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892" + +static const ftp_auth_creds_t s_creds = { "admin", DEF_HASH, 1u }; + +/* ===================== TC-FA-001:user_match ===================== */ + +static void tc_user_match(void) +{ + CHECK(ftp_auth_user_match("admin", "admin") == 1, "TC-FA-001a 相等匹配"); + CHECK(ftp_auth_user_match("admin", "ADMIN") == 1, "TC-FA-001b 大小写不敏感"); + CHECK(ftp_auth_user_match("admin", "root") == 0, "TC-FA-001c 不等拒绝"); + CHECK(ftp_auth_user_match("admin", "admin2") == 0, "TC-FA-001d 前缀不算匹配"); + CHECK(ftp_auth_user_match("admin", NULL) == 0, "TC-FA-001e NULL 拒绝"); +} + +/* ===================== TC-FA-002..004:基本判定 ===================== */ + +static void tc_basic(void) +{ + ftp_auth_state_t st; + + (void)memset(&st, 0, sizeof(st)); + CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, 1000u) == FTP_AUTH_OK, + "TC-FA-002 对用户名+对口令 → OK"); + CHECK(st.fail_count == 0u, "TC-FA-002b 成功后计数为 0"); + + (void)memset(&st, 0, sizeof(st)); + CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, 1000u) == FTP_AUTH_FAIL, + "TC-FA-003 错口令 → FAIL"); + CHECK(st.fail_count == 1u, "TC-FA-003b 失败计数 1"); + + (void)memset(&st, 0, sizeof(st)); + CHECK(ftp_auth_check(&s_creds, 0u, "admin", &st, 1000u) == FTP_AUTH_FAIL, + "TC-FA-004 错用户名+对口令 → FAIL(哈希路径已走)"); +} + +/* ===================== TC-FA-005..006:锁定与解锁 ===================== */ + +static void tc_lock(void) +{ + ftp_auth_state_t st; + uint32_t t = 10000u; + + (void)memset(&st, 0, sizeof(st)); + for (int i = 0; i < 5; i++) { + CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, t) == FTP_AUTH_FAIL, + "TC-FA-005 第 1..5 次失败 → FAIL"); + } + CHECK(st.fail_count == 5u, "TC-FA-005b 计数到达 FAIL_MAX"); + /* 锁定期内:对口令也 LOCKED,且不再计数 */ + CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, t + 1u) == FTP_AUTH_LOCKED, + "TC-FA-005c 锁定期内对口令 → LOCKED"); + CHECK(st.fail_count == 5u, "TC-FA-005d LOCKED 不改变计数"); + /* 59999 tick:仍在窗内(差值 < 60000) */ + CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, + st.lock_until_tick - 1u) == FTP_AUTH_LOCKED, + "TC-FA-005e 窗内最后 1 tick 仍 LOCKED"); + /* 期满:恰好到达 lock_until → 放行 */ + CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, + st.lock_until_tick) == FTP_AUTH_OK, + "TC-FA-006 期满对口令 → OK"); + CHECK(st.fail_count == 0u, "TC-FA-006b 解锁成功后计数清零"); +} + +/* ===================== TC-FA-007:成功清零计数 ===================== */ + +static void tc_reset(void) +{ + ftp_auth_state_t st; + uint32_t t = 5000u; + + (void)memset(&st, 0, sizeof(st)); + for (int i = 0; i < 3; i++) { + (void)ftp_auth_check(&s_creds, 1u, "wrong", &st, t); + } + CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, t) == FTP_AUTH_OK, + "TC-FA-007a 3 败后成功 → OK"); + CHECK(st.fail_count == 0u, "TC-FA-007b 成功清零"); + for (int i = 0; i < 4; i++) { + CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, t) == FTP_AUTH_FAIL, + "TC-FA-007c 清零后再 4 败仍 FAIL(未锁)"); + } + CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, t) == FTP_AUTH_FAIL, + "TC-FA-007d 第 5 败 FAIL 并上锁"); + CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, t + 1u) == FTP_AUTH_LOCKED, + "TC-FA-007e 连续 5 败后 LOCKED"); +} + +/* ===================== TC-FA-008:tick 32 位回绕 ===================== */ + +static void tc_wraparound(void) +{ + ftp_auth_state_t st; + + /* 手工构造:锁定截止在 0xFFFFFFF0(距 32 位翻转仅 16 tick) */ + (void)memset(&st, 0, sizeof(st)); + st.fail_count = 5u; + st.lock_until_tick = 0xFFFFFFF0u; + /* 回绕前 240 tick:仍在窗内 */ + CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, 0xFFFFFF00u) == FTP_AUTH_LOCKED, + "TC-FA-008a 回绕前窗内 → LOCKED"); + /* 回绕后 32 tick:now=0x10,(int32_t)(0x10-0xFFFFFFF0)=+32 → 期满 */ + CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, 0x00000010u) == FTP_AUTH_OK, + "TC-FA-008b 回绕后期满 → OK(有符号差抗回绕)"); +} + +/* ===================== TC-FA-009..010:fail-closed 边界 ===================== */ + +static void tc_fail_closed(void) +{ + ftp_auth_state_t st; + ftp_auth_creds_t bad_creds = { "admin", "zz2494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892", 1u }; + + (void)memset(&st, 0, sizeof(st)); + CHECK(ftp_auth_check(&bad_creds, 1u, "admin", &st, 1000u) == FTP_AUTH_FAIL, + "TC-FA-009 非法 hex 配置 → 恒 FAIL(无人能登录,fail-closed)"); + + (void)memset(&st, 0, sizeof(st)); + CHECK(ftp_auth_check(&s_creds, 1u, "", &st, 1000u) == FTP_AUTH_FAIL, + "TC-FA-010a 空口令 → FAIL"); + CHECK(ftp_auth_check(&s_creds, 1u, NULL, &st, 1000u) == FTP_AUTH_FAIL, + "TC-FA-010b NULL 口令 → FAIL"); + CHECK(ftp_auth_check(NULL, 1u, "admin", &st, 1000u) == FTP_AUTH_FAIL, + "TC-FA-010c NULL creds → FAIL"); + CHECK(ftp_auth_check(&s_creds, 1u, "admin", NULL, 1000u) == FTP_AUTH_FAIL, + "TC-FA-010d NULL state → FAIL"); +} + +/* ===================== main ===================== */ + +int main(void) +{ + printf("=== FTP Auth Tests ===\n"); + + printf("-- user match --\n"); + tc_user_match(); + printf("-- basic check --\n"); + tc_basic(); + printf("-- lock / unlock --\n"); + tc_lock(); + printf("-- success reset --\n"); + tc_reset(); + printf("-- tick wraparound --\n"); + tc_wraparound(); + printf("-- fail-closed --\n"); + tc_fail_closed(); + + printf("\n=== FTP Auth Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/ftp_auth/run_unit_test.py b/test/host/ftp_auth/run_unit_test.py new file mode 100644 index 00000000..7b3c703f --- /dev/null +++ b/test/host/ftp_auth/run_unit_test.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""FTP 鉴权状态机(lftpd_auth)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/ftp_auth/ftp_auth_test.c(它以 #include 纳入 + **生产源文件** Drivers/BSP/NET/lftpd/lftpd_auth.c 与 App/util/sha256.c), + 在 PC 上离线跑口令校验/失败计数/限时锁定/tick 回绕,失败即非 0 退出。 + 零硬件、零烧录、毫秒级。 + + 工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness + `test/host/common/unit_test_harness.py`(本文件只做声明)。 + +为什么要主机侧单测(而不是只用台架): + 锁定 60s / 解锁 / 成功清零 / tick 32 位回绕是"防口令爆破"契约核心, + 板端每验一条要真等 60s、回绕要跑满 49.7 天;lftpd_auth 的纯函数设计 + (tick 参数注入)正是为了让这些分支在主机侧毫秒级穷举。 + 两被测模块零平台依赖,无需任何桩。 + +用法(仓库根目录): + python test/host/ftp_auth/run_unit_test.py + 或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/ftp_auth/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:Drivers/BSP/NET/lftpd/lftpd_auth.h;创建日期: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))) +# harness 位于 test/host/common/ +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="FTP Auth Tests", + src=os.path.join(HERE, "ftp_auth_test.c"), + include_dirs=[ + os.path.join(ROOT, "Drivers", "BSP", "NET", "lftpd"), + os.path.join(ROOT, "App", "util"), + HERE, + ], + summary_marker="=== FTP Auth Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/ftp_auth_bench.py b/test/host/ftp_auth_bench.py new file mode 100644 index 00000000..0ae78557 --- /dev/null +++ b/test/host/ftp_auth_bench.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""FTP 鉴权台架回归(N-19,需真机在线) + +验证固件侧 lftpd 鉴权集成的完整行为矩阵(主机单测 test/host/ftp_auth 覆盖 +不到的"协议接线"部分:命令门、530/331/230/550 响应码、跨会话锁定): + + T1 匿名登录(anonymous) → 期望 530 拒 + T2 未登录直接 NLST → 期望 530(命令门) + T3 对用户名 + 错口令 ×5 → 期望 530 ×5(并触发锁定) + T4 锁定窗内正确口令 → 期望 530(LOCKED,不算哈希) + T5 等待锁定期满后正确口令 → 期望 230(60s + 余量) + T6 登录后 NLST / STOR / RETR / DELE → 期望全通(出厂 write=1) + T7 会话结束重连、错口令一次 → 期望 530 且计数已清零(T5 成功清零) + +注意:CH395F 单连接——每步用完必须 quit() 释放 socket,否则下一步连不上; + 跑之前确保 UI 远程显示(7003)/对时(8888)客户端未占用 socket。 + +用法(设备默认 192.168.1.100,出厂口令 admin/admin): + & '.venv\\Scripts\\python.exe' test/host/ftp_auth_bench.py + 改过口令: ... --pwd <新口令> + 跳过 60s 锁定等待(只验锁定触发不验解锁): ... --skip-unlock + +退出码:0=全部通过 / 1=任一失败 / 2=环境错误(连不上设备) +创建日期:2026-10-11 +""" + +import argparse +import io +import socket +import sys +import time +from ftplib import FTP, error_perm + +LOCK_WAIT_S = 62 # 固件锁定 60 s + 余量 + + +def _try_login(host, port, timeout, user, pwd): + """尝试一次登录;返回 (ok, code_or_exc)。失败不断言连接状态。 + + 单连接 socket:上一次会话结束后 net 层 auto_relisten ~50 ms,但 accept + 失败会走"毁监听→重建"路径(3 s 重试窗)→ connect 被拒时重试 3 次。 + """ + f = FTP() + last_err = None + for attempt in range(3): + try: + time.sleep(0.5) + f.connect(host, port, timeout=timeout) + break + except (ConnectionRefusedError, socket.timeout, OSError) as e: + last_err = e + f = FTP() + else: + return None, repr(last_err), None + try: + try: + f.login(user, pwd) + return True, "230", f + except error_perm as e: + f.close() + return False, str(e), None + except (socket.timeout, OSError) as e: + return None, repr(e), None + + +def main(argv=None): + ap = argparse.ArgumentParser(description="FTP auth bench matrix (N-19)") + ap.add_argument("--host", default="192.168.1.100") + ap.add_argument("--port", type=int, default=21) + ap.add_argument("--timeout", type=int, default=15) + ap.add_argument("--user", default="admin") + ap.add_argument("--pwd", default="admin") + ap.add_argument("--skip-unlock", action="store_true", + help="不验 60s 解锁(省 1 分钟;锁定触发仍验)") + args = ap.parse_args(argv) + + fails = [] + + def expect(name, cond, detail=""): + tag = "PASS" if cond else "FAIL" + print(" [%s] %s %s" % (tag, name, detail)) + if not cond: + fails.append(name) + + # ---- T1 匿名登录被拒 ---- + ok, code, conn = _try_login(args.host, args.port, args.timeout, "anonymous", "x@x") + if ok is None: + print("[env] 连不上 %s:%d → %s" % (args.host, args.port, code)) + return 2 + expect("T1 匿名登录 → 530", (ok is False) and code.startswith("530"), code) + if conn: + conn.quit() + + # ---- T2 未登录直接 NLST → 530(命令门)---- + time.sleep(0.5) # 单连接 socket:等 auto_relisten 沉降(~50 ms + 余量) + f = FTP() + f.connect(args.host, args.port, timeout=args.timeout) + try: + f.sendcmd("NLST") + expect("T2 未登录 NLST → 530", False, "居然放行") + except error_perm as e: + expect("T2 未登录 NLST → 530", str(e).startswith("530"), str(e)) + except Exception as e: # noqa: BLE001 + expect("T2 未登录 NLST → 530", False, repr(e)) + # ⚠ 必须 close:单连接模型下不关 = 占住 socket,后续步骤全被拒 + # (2026-10-11 台架实测:QUIT 后不 close → 芯片视角旧连接仍 ESTABLISHED) + try: + f.quit() + except Exception: # noqa: BLE001 + try: + f.close() + except Exception: # noqa: BLE001 + pass + time.sleep(0.5) + + # ---- T3 错口令 ×5 → 530 ×5 ---- + for i in range(1, 6): + ok, code, conn = _try_login(args.host, args.port, args.timeout, + args.user, "wrong-pass-%d" % i) + expect("T3.%d 错口令 → 530" % i, (ok is False) and code.startswith("530"), code) + if conn: + conn.quit() + + # ---- T4 锁定窗内正确口令 → 530 ---- + ok, code, conn = _try_login(args.host, args.port, args.timeout, args.user, args.pwd) + expect("T4 锁定窗内对口令 → 530", (ok is False) and code.startswith("530"), code) + if conn: + conn.quit() + + if args.skip_unlock: + print("[skip] 未验 60s 解锁(--skip-unlock)") + else: + # ---- T5 期满正确口令 → 230 ---- + print(" [wait] 等锁定期满 %d s ..." % LOCK_WAIT_S) + time.sleep(LOCK_WAIT_S) + ok, code, conn = _try_login(args.host, args.port, args.timeout, + args.user, args.pwd) + expect("T5 期满对口令 → 230", ok is True, code) + if conn: + # ---- T6 登录后读写全通(出厂 write=1)---- + try: + names = conn.nlst() + expect("T6a NLST", len(names) >= 0, "%d 项" % len(names)) + payload = b"N19-auth-bench-" + b"X" * 500 + conn.storbinary("STOR n19t.tmp", io.BytesIO(payload)) + got = io.BytesIO() + conn.retrbinary("RETR n19t.tmp", got.write) + expect("T6b STOR/RETR 往返一致", got.getvalue() == payload) + conn.delete("n19t.tmp") + expect("T6c DELE", True) + except Exception as e: # noqa: BLE001 + expect("T6 登录后读写", False, repr(e)) + # ---- T7 成功已清零计数:本会话退出后错一次只算 1(不锁)---- + conn.quit() + ok, code, conn = _try_login(args.host, args.port, args.timeout, + args.user, "one-wrong") + expect("T7 成功后错 1 次 → 530 且未锁(下次对口令可登)", + (ok is False) and code.startswith("530"), code) + if conn: + conn.quit() + ok, code, conn = _try_login(args.host, args.port, args.timeout, + args.user, args.pwd) + expect("T7b 紧接对口令 → 230", ok is True, code) + if conn: + conn.quit() + + print("\nVERDICT: %s (%d fail)" % ("PASS" if not fails else "FAIL", len(fails))) + return 0 if not fails else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/host/ftp_regression.py b/test/host/ftp_regression.py index 84542c4a..4dd4657a 100644 --- a/test/host/ftp_regression.py +++ b/test/host/ftp_regression.py @@ -30,6 +30,10 @@ def main(argv=None): ap.add_argument("--host", default="192.168.1.100", help="device IP") ap.add_argument("--port", type=int, default=21, help="FTP control port") ap.add_argument("--timeout", type=int, default=20, help="socket timeout (s)") + # N-19(2026-10-11):FTP 已强制鉴权,匿名登录被 530 拒;默认口令 admin/admin + # (出厂值,见 ftp_cfg.h),改过 [ftp] pwd_hash 后用这两个参数覆盖 + ap.add_argument("--user", default="admin", help="FTP user ([ftp] user)") + ap.add_argument("--pwd", default="admin", help="FTP password") args = ap.parse_args(argv) results = [] @@ -52,7 +56,7 @@ def main(argv=None): f = FTP() print("[ftp] connecting %s:%d ..." % (args.host, args.port)) f.connect(args.host, args.port, timeout=args.timeout) - f.login("anonymous", "x@x") + f.login(args.user, args.pwd) print("[ftp] login ok: %s" % f.getwelcome()) # 同一会话内连续 8 次数据连接(含两次大文件)—— D20 的失败点是第 2 次 diff --git a/test/host/iec104/iec104_test.c b/test/host/iec104/iec104_test.c new file mode 100644 index 00000000..504f8f58 --- /dev/null +++ b/test/host/iec104/iec104_test.c @@ -0,0 +1,920 @@ +/* + * 模块名称:IEC 60870-5-104 被控站协议栈主机侧单元测试 + * 模块功能:在 PC 上对 App/protocol/iec104 全栈(asdu + pts + link)做离线验证。 + * + * 被测对象:真实源文件(#include 原样纳入): + * iec104_asdu.c + iec104_pts.c + iec104_link.c(B4 协议核心) + * prot_set.c + prot_elm.c + phasor.c + sample_ring.c(板端默认源 + * 的链接依赖;定值两段写走**真实 prot_set**) + * 数据源经 iec104_pts_bind_src 注入假源(YC/YX/YK 可控), + * 发送经 send_apdu 回调全量捕获,时基由测试注入。 + * + * 覆盖点(docs/子系统设计/IEC104上行设计说明.md §7): + * TC-104-001 CP56Time2a 往返 / 闰年 / IV / 钳位 + * TC-104-002 IOA 3B 与短浮点小端往返 + * TC-104-003 APCI 重组:半帧 / 粘帧 / 坏起始符 / 坏长度 + * TC-104-004 STARTDT 握手 → RUN + * TC-104-005 OPEN 态 I 帧忽略(序号不消费) + * TC-104-006 总召全流程:actcon→4 分片(首召 COT=4)→actterm;二召 COT=2;RBT→COT=4 + * TC-104-007 遥信变位 COV(COT=3);无效位不触发 + * TC-104-008 遥测死区 COV:死区内不发 / 越死区发 + * TC-104-009 k 窗:12 未确认停发;S 确认后恢复 + * TC-104-010 w 阈:收满 8 发 S + * TC-104-011 t2 到期发 S + * TC-104-012 t3 空闲发 TESTFR act;未答 t1 超时 → IDLE;con 清挂起不误杀 + * TC-104-013 遥控两段式:选择/执行成功;失配 PN;拒绝类选择段即 PN + * TC-104-014 定值两段写(真实 prot_set):越界 PN;执行后 get_f + selfcheck + * TC-104-015 C_RD:YC 命中 / 定值命中 / 未知 IOA COT14+EVI + * TC-104-016 C_CS:clock_sync 收正确 epoch;IV → PN;clock_sync 失败 → PN + * TC-104-017 未知类型 COT12+EVI;错公共地址 COT13+EVI + * TC-104-018 STOPDT:数据停发,TESTFR 仍应答 + * TC-104-019 序号错 → IDLE(链路故障) + * TC-104-020 pts:无效通道 IV / 未就绪 NT / 无效遥信 IV / is_dp / 表尺寸 + * TC-104-021 C_TS / C_RP actcon + * TC-104-022 周期上送 COT=1(YC 组 + 频率) + * TC-104-023 k 窗满 COV 不丢事件(pend 暂存,窗开后补发) + * + * 通过:全部 TC PASS(main 返回 0)。 + * 创建日期:2026-10-12(B4) + */ +#include +#include +#include + +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 "iec104_asdu.c" +#include "prot_set.c" +#include "sample_ring.c" +#include "phasor.c" +#include "prot_elm.c" +#include "iec104_pts.c" +#include "iec104_link.c" + +/* ---------------- 假数据源(定值转发真实 prot_set) ---------------- */ +static float s_f_yc[9]; +static int s_f_yc_rc[9]; +static uint64_t s_f_bits; +static uint64_t s_f_valid; +static uint32_t s_f_yk_ioa; +static uint8_t s_f_yk_dpc; +static uint8_t s_f_yk_sel; + +static int f_yc(uint8_t idx, float *out) +{ + if (idx >= 9u) { + return -1; + } + if (s_f_yc_rc[idx] != 0) { + return s_f_yc_rc[idx]; + } + *out = s_f_yc[idx]; + return 0; +} +static uint64_t f_bits(void) { return s_f_bits; } +static uint64_t f_valid(void) { return s_f_valid; } +static int f_yk(uint32_t ioa, uint8_t dpc, uint8_t sel) +{ + s_f_yk_ioa = ioa; + s_f_yk_dpc = dpc; + s_f_yk_sel = sel; + if (ioa == 0x600Bu) { + /* YK12 全局软压板(B7):转真实板端源(prot_set_set_sealed 原子路径) */ + return s_src_yk_exec(ioa, dpc, sel); + } + return (ioa == 0x600Au) ? 0 : -1; +} +static int f_set_read(uint32_t addr, float *out) +{ + return s_src_set_read(addr, out); +} +static int f_set_write(uint32_t addr, float v, uint8_t sel) +{ + return s_src_set_write(addr, v, sel); +} +static const iec104_pts_src_t g_fake = { + f_yc, f_bits, f_valid, f_yk, f_set_read, f_set_write +}; + +/* ---------------- 发送捕获 + 对时假回调 ---------------- */ +#define CAP_MAX 48 +#define CAP_SZ 260 +static uint8_t s_cap[CAP_MAX][CAP_SZ]; +static int s_cap_n; +static uint64_t s_clock_ms; +static int s_clock_rc; + +static void cap_send(void *ctx, const uint8_t *apdu, uint16_t len) +{ + (void)ctx; + if ((s_cap_n < CAP_MAX) && (len <= CAP_SZ)) { + (void)memcpy(s_cap[s_cap_n], apdu, len); + s_cap_n++; + } +} +static int f_clock(void *ctx, uint64_t ms) +{ + (void)ctx; + s_clock_ms = ms; + return s_clock_rc; +} + +static iec104_link_t g_lk; +static const iec104_link_cfg_t g_cfg = { 1u, cap_send, f_clock }; + +/* ---------------- 主站侧模拟(N(R) 单调,照真实主站) ---------------- */ +static uint16_t m_ns; +static uint16_t m_nr; + +static void m_reset(void) +{ + m_ns = 0u; + m_nr = 0u; + s_cap_n = 0; +} +static void feed_u(uint8_t q, uint32_t now) +{ + uint8_t b[6]; + + b[0] = 0x68u; b[1] = 4u; b[2] = q; b[3] = 0u; b[4] = 0u; b[5] = 0u; + iec104_link_on_rx(&g_lk, b, 6u, now); +} +static void feed_i(const uint8_t *asdu, uint16_t len, uint32_t now) +{ + uint8_t b[6u + 250u]; + uint16_t fs = (uint16_t)(m_ns << 1); + uint16_t fr = (uint16_t)(m_nr << 1); /* N(R) 单调(feed_s 推进) */ + + b[0] = 0x68u; + b[1] = (uint8_t)(4u + len); + b[2] = (uint8_t)(fs & 0xFFu); + b[3] = (uint8_t)(fs >> 8); + b[4] = (uint8_t)(fr & 0xFFu); + b[5] = (uint8_t)(fr >> 8); + (void)memcpy(&b[6], asdu, len); + iec104_link_on_rx(&g_lk, b, (uint16_t)(6u + len), now); + m_ns = (uint16_t)((m_ns + 1u) % 32768u); +} +static void feed_s(uint16_t nr, uint32_t now) +{ + uint8_t b[6]; + uint16_t f = (uint16_t)(nr << 1); + + b[0] = 0x68u; b[1] = 4u; b[2] = 1u; b[3] = 0u; + b[4] = (uint8_t)(f & 0xFFu); + b[5] = (uint8_t)(f >> 8); + m_nr = nr; /* 主站确认推进(单调) */ + iec104_link_on_rx(&g_lk, b, 6u, now); +} +static uint16_t mk_asdu(uint8_t *b, uint8_t type, uint8_t vsq, uint8_t cot, + uint8_t cot_hi, uint8_t with_ioa, uint32_t ioa, + const uint8_t *body, uint16_t blen) +{ + uint16_t n = iec104_asdu_put_hdr(b, type, vsq, cot, cot_hi, 1u); + + if (with_ioa != 0u) { + iec104_put_ioa(&b[n], ioa); + n += 3u; + } + if (blen > 0u) { + (void)memcpy(&b[n], body, blen); + n += blen; + } + return n; +} + +/* ---------------- 复位 / 握手 ---------------- */ +static void reset_all(uint32_t now) +{ + prot_set_init(); + prot_elm_init(); + iec104_pts_init(); + iec104_pts_bind_src(&g_fake); + (void)memset(s_f_yc, 0, sizeof(s_f_yc)); + (void)memset(s_f_yc_rc, 0, sizeof(s_f_yc_rc)); + s_f_bits = 0u; + s_f_valid = (1uLL << 14) | (1uLL << 15) | (1uLL << 16) | (1uLL << 17) + | (1uLL << 22) | (1uLL << 30); + s_clock_rc = 0; + s_clock_ms = 0u; + iec104_link_init(&g_lk, &g_cfg, NULL); + iec104_link_open(&g_lk, now); + m_reset(); +} +static void handshake(uint32_t now) +{ + feed_u(0x07u, now); /* STARTDT act */ +} + +/* ---------------- 捕获检查 ---------------- */ +typedef struct { + uint8_t type, vsq, cot, cot_hi, oa; + uint32_t ca, ioa; + const uint8_t *obj; /* ioa 之后的信息体 */ + uint16_t obj_len; +} cap_t; + +static int cap_i_count(void) +{ + int k, n = 0; + + for (k = 0; k < s_cap_n; k++) { + if ((s_cap[k][2] & 0x01u) == 0u) { + n++; + } + } + return n; +} +static int cap_u_count(uint8_t q) +{ + int k, n = 0; + + for (k = 0; k < s_cap_n; k++) { + if ((s_cap[k][2] & 0x03u) == 0x03u && s_cap[k][2] == q) { + n++; + } + } + return n; +} +static int cap_has_s(uint16_t *nr_out) +{ + int k; + + for (k = s_cap_n - 1; k >= 0; k--) { + const uint8_t *p = s_cap[k]; + + if ((p[2] & 0x03u) == 0x01u) { + *nr_out = (uint16_t)(((uint16_t)p[4] | ((uint16_t)p[5] << 8)) >> 1); + return 1; + } + } + return 0; +} +static int cap_i(int n, cap_t *o) +{ + int k, seen = 0; + + for (k = 0; k < s_cap_n; k++) { + const uint8_t *p = s_cap[k]; + uint16_t lf = p[1]; + iec104_asdu_hdr_t h; + + if ((p[2] & 0x01u) != 0u) { + continue; + } + if (seen == n) { + if (iec104_asdu_parse_hdr(&p[6], (uint16_t)(lf - 4u), &h) != 0) { + return -1; + } + o->type = h.type; + o->vsq = h.vsq; + o->cot = h.cot; + o->cot_hi = h.cot_hi; + o->oa = h.oa; + o->ca = h.ca; + if (h.obj_len >= 3u) { + o->ioa = iec104_get_ioa(h.obj); + o->obj = h.obj + 3u; + o->obj_len = (uint16_t)(h.obj_len - 3u); + } else { + o->ioa = 0u; + o->obj = h.obj; + o->obj_len = 0u; + } + return 0; + } + seen++; + } + return -1; +} + +/* ============================ 测试用例 ============================ */ + +static void tc_cp56(void) +{ + uint8_t b[7]; + uint64_t e1 = 946684800000ULL; /* 2000-01-01 00:00:00.000 */ + uint64_t e2 = 1709188496789ULL; /* 2024-02-29 06:34:56.789(闰日) */ + uint64_t back = 0u; + int rc; + + iec104_epoch_ms_to_cp56(e1, b); + rc = iec104_cp56_to_epoch_ms(b, &back); + CHECK((rc == 0) && (back == e1), "TC-001 CP56 往返 2000-01-01"); + iec104_epoch_ms_to_cp56(e2, b); + back = 0u; + rc = iec104_cp56_to_epoch_ms(b, &back); + CHECK((rc == 0) && (back == e2), "TC-001 CP56 往返闰日 2024-02-29"); + b[2] |= 0x80u; + CHECK(iec104_cp56_to_epoch_ms(b, &back) == -2, "TC-001 IV → -2"); + iec104_epoch_ms_to_cp56(0u, b); /* 早于 2000 → 钳位 */ + back = 0u; + (void)iec104_cp56_to_epoch_ms(b, &back); + CHECK(back == e1, "TC-001 早于 2000 钳位"); +} + +static void tc_ioa_float(void) +{ + uint8_t b[8]; + + iec104_put_ioa(b, 0x123456u); + CHECK((iec104_get_ioa(b) == 0x123456u) && (b[0] == 0x56u) && (b[1] == 0x34u) + && (b[2] == 0x12u), "TC-002 IOA 3B 小端"); + iec104_put_float(b, 1.0f); + CHECK((b[0] == 0x00u) && (b[1] == 0x00u) && (b[2] == 0x80u) + && (b[3] == 0x3Fu), "TC-002 float IEEE754 小端"); + iec104_put_float(b, -12.5f); + CHECK(iec104_get_float(b) == -12.5f, "TC-002 float 往返"); +} + +static void tc_apci(void) +{ + uint32_t now = 1000u; + + reset_all(now); + { + static const uint8_t b[6] = { 0x68u, 4u, 0x07u, 0u, 0u, 0u }; + + iec104_link_on_rx(&g_lk, b, 3u, now); + CHECK(cap_u_count(0x0Bu) == 0, "TC-003 半帧不触发"); + iec104_link_on_rx(&g_lk, &b[3], 3u, now); + CHECK(cap_u_count(0x0Bu) == 1, "TC-003 半帧重组 → con"); + } + { + static const uint8_t b[12] = { + 0x68u, 4u, 0x43u, 0u, 0u, 0u, /* TESTFR act */ + 0x68u, 4u, 0x13u, 0u, 0u, 0u /* STOPDT act */ + }; + + iec104_link_on_rx(&g_lk, b, 12u, now); + CHECK((cap_u_count(0x83u) == 1) + && (iec104_link_state(&g_lk) == IEC104_ST_STOPPED), + "TC-003 粘帧逐处 + STOPDT 生效"); + } + reset_all(now); + { + static const uint8_t b[6] = { 0x69u, 4u, 0x07u, 0u, 0u, 0u }; + + iec104_link_on_rx(&g_lk, b, 6u, now); + CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-003 坏起始符 → IDLE"); + } + reset_all(now); + { + static const uint8_t b[6] = { 0x68u, 3u, 0x07u, 0u, 0u, 0u }; + + iec104_link_on_rx(&g_lk, b, 6u, now); + CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-003 坏长度 → IDLE"); + } +} + +static void tc_startdt(void) +{ + uint32_t now = 1000u; + + reset_all(now); + CHECK(iec104_link_state(&g_lk) == IEC104_ST_OPEN, "TC-004 open → OPEN"); + handshake(now); + CHECK((cap_u_count(0x0Bu) == 1) + && (iec104_link_state(&g_lk) == IEC104_ST_RUN), "TC-004 STARTDT → RUN"); +} + +static void tc_open_ignore(void) +{ + uint32_t now = 1000u; + uint8_t a[16]; + uint16_t n; + + reset_all(now); + n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x4011u, NULL, 0u); + feed_i(a, n, now); + CHECK((cap_i_count() == 0) && (iec104_link_state(&g_lk) == IEC104_ST_OPEN), + "TC-005 OPEN 态 I 帧忽略"); + handshake(now); + m_ns = 0u; /* OPEN 忽略不消费序号 → 主站仍从 0 起 */ + feed_i(a, n, now); /* ns=0 仍有效(序号未被消费) */ + CHECK(cap_i_count() == 1, "TC-005 RUN 后同 ns 正常应答"); +} + +static void tc_interrogation(void) +{ + uint32_t now = 5000u; + uint8_t a[32]; + uint8_t body[1]; + uint16_t n; + cap_t c; + + reset_all(now); + s_f_yc[0] = 111.0f; + s_f_yc_rc[1] = -1; /* idx1(1Ub)无效通道 */ + handshake(now); + body[0] = IEC104_RQT_GROUP1; + n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u); + feed_i(a, n, now); + CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 100u) + && (c.cot == IEC104_COT_ACTCON), "TC-006 actcon 即发 COT7"); + iec104_link_poll(&g_lk, now); + CHECK(cap_i_count() == 5, "TC-006 首 poll 发满预算 4 分片"); + CHECK((cap_i(1, &c) == 0) && (c.type == 3u) && (c.cot == IEC104_COT_INIT) + && (c.vsq == (uint8_t)(IEC104_VSQ_SQ | 4u)) && (c.ioa == 1u), + "TC-006 YX 双点组 首召 COT=4"); + CHECK((cap_i(2, &c) == 0) && (c.type == 1u) + && (c.vsq == (uint8_t)(IEC104_VSQ_SQ | 29u)) && (c.ioa == 5u), + "TC-006 YX 单点组 29 点"); + CHECK((cap_i(3, &c) == 0) && (c.type == 13u) + && (c.vsq == (uint8_t)(IEC104_VSQ_SQ | 8u)) && (c.ioa == 0x4011u) + && (c.obj_len == 40u), "TC-006 YC 组 8 点 40B"); + if (cap_i(3, &c) == 0) { + CHECK((iec104_get_float(c.obj) == 111.0f) && (c.obj[4] == 0u), + "TC-006 YC 值 111.0 QDS=0"); + CHECK(c.obj[9] == IEC104_QDS_IV, "TC-006 无效通道 QDS=IV"); + } + CHECK((cap_i(4, &c) == 0) && (c.ioa == 0x401Du) && (c.vsq == 1u), + "TC-006 频率单点 SQ=0"); + iec104_link_poll(&g_lk, now + 100u); + CHECK((cap_i_count() == 6) && (cap_i(5, &c) == 0) && (c.type == 100u) + && (c.cot == IEC104_COT_ACTTERM), "TC-006 actterm 收尾"); + /* 二召:init 已清 → COT=2 */ + n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u); + feed_i(a, n, now + 200u); + iec104_link_poll(&g_lk, now + 200u); + CHECK((cap_i(7, &c) == 0) && (c.type == 3u) && (c.cot == IEC104_COT_BG_SCAN), + "TC-006 二召 COT=2"); + /* RBT 位 → COT=4(actcon3 是第 12 个未确认帧 → 先 S 确认窗才开) */ + n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, IEC104_COT_BIT_RBT, + 1u, 0u, body, 1u); + feed_i(a, n, now + 300u); + feed_s(12u, now + 300u); + iec104_link_poll(&g_lk, now + 300u); + CHECK((cap_i(12, &c) == 0) && (c.type == 3u) && (c.cot == IEC104_COT_INIT), + "TC-006 RBT → COT=4"); +} + +static void tc_cov_yx(void) +{ + uint32_t now = 1000u; + cap_t c; + + reset_all(now); + handshake(now); + iec104_link_poll(&g_lk, now); /* 建快照 */ + CHECK(cap_i_count() == 0, "TC-007 首 poll 无自发"); + s_f_bits |= (1uLL << 15); /* YX16(idx15)动作 */ + iec104_link_poll(&g_lk, now + 100u); + CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 1u) + && (c.cot == IEC104_COT_SPONT) && (c.ioa == 16u) + && (c.obj_len == 1u) && (c.obj[0] == 1u), "TC-007 YX16 变位 COT3"); + s_f_bits &= ~(1uLL << 15); /* 复归 */ + iec104_link_poll(&g_lk, now + 200u); + CHECK((cap_i_count() == 2) && (cap_i(1, &c) == 0) && (c.ioa == 16u) + && (c.obj[0] == 0u), "TC-007 YX16 复归再变"); + s_f_bits |= (1uLL << 0); /* idx0 无效位 → 不触发 */ + iec104_link_poll(&g_lk, now + 300u); + CHECK(cap_i_count() == 2, "TC-007 无效位变位不触发"); +} + +static void tc_cov_yc(void) +{ + uint32_t now = 1000u; + cap_t c; + + reset_all(now); + handshake(now); + s_f_yc[0] = 100.0f; + iec104_link_poll(&g_lk, now); /* 快照 = 100 */ + s_f_yc[0] = 105.0f; /* 死区 10 内 */ + iec104_link_poll(&g_lk, now + 100u); + CHECK(cap_i_count() == 0, "TC-008 死区内不上送"); + s_f_yc[0] = 130.0f; /* 130-105=25 > 10 */ + iec104_link_poll(&g_lk, now + 200u); + CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 13u) + && (c.cot == IEC104_COT_SPONT) && (c.ioa == 0x4011u) + && (iec104_get_float(c.obj) == 130.0f), "TC-008 越死区上送"); +} + +static void feed_rd(uint32_t now) +{ + uint8_t a[16]; + uint16_t n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, + 1u, 0x4011u, NULL, 0u); + + feed_i(a, n, now); +} + +static void tc_k_window(void) +{ + uint32_t now = 1000u; + int i; + + reset_all(now); + handshake(now); + for (i = 0; i < 12; i++) { + feed_rd(now); + } + CHECK(cap_i_count() == 12, "TC-009 k=12 封顶"); + feed_rd(now); + CHECK(cap_i_count() == 12, "TC-009 第 13 个被 k 窗抑制"); + feed_s(12u, now); /* 全确认 → 窗开 */ + feed_rd(now); + CHECK(cap_i_count() == 13, "TC-009 S 确认后恢复发送"); +} + +static void tc_w_thresh(void) +{ + uint32_t now = 1000u; + uint16_t nr = 0xFFFFu; + int i; + + reset_all(now); + handshake(now); + for (i = 0; i < 7; i++) { + feed_rd(now); + } + CHECK(cap_has_s(&nr) == 0, "TC-010 收 7 未发 S"); + feed_rd(now); /* 第 8 个 → w 阈 */ + CHECK((cap_has_s(&nr) == 1) && (nr == 8u), "TC-010 w=8 → S nr=8"); +} + +static void tc_t2(void) +{ + uint32_t now = 1000u; + uint16_t nr = 0xFFFFu; + + reset_all(now); + handshake(now); + feed_rd(now); /* rx_unacked=1,t2 起爆 */ + iec104_link_poll(&g_lk, now + 9999u); + CHECK(cap_has_s(&nr) == 0, "TC-011 t2 未到期不发 S"); + iec104_link_poll(&g_lk, now + 10001u); + CHECK((cap_has_s(&nr) == 1) && (nr == 1u), "TC-011 t2 到期发 S"); +} + +static void tc_t3_t1(void) +{ + uint32_t now = 1000u; + + reset_all(now); + handshake(now); + iec104_link_poll(&g_lk, now + 19999u); + CHECK(cap_u_count(0x43u) == 0, "TC-012 t3 未到期无 TESTFR"); + iec104_link_poll(&g_lk, now + 20001u); + CHECK(cap_u_count(0x43u) == 1, "TC-012 t3 到期发 TESTFR act"); + iec104_link_poll(&g_lk, now + 20001u + 15001u); + CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-012 TESTFR 未答 t1 → IDLE"); + /* con 清挂起 → 不误杀 */ + reset_all(now); + handshake(now); + iec104_link_poll(&g_lk, now + 20001u); + CHECK(cap_u_count(0x43u) == 1, "TC-012 重连 t3 再触发"); + feed_u(0x83u, now + 20100u); /* TESTFR con */ + iec104_link_poll(&g_lk, now + 35000u); + CHECK(iec104_link_state(&g_lk) == IEC104_ST_RUN, "TC-012 con 清挂起后 t1 不误杀"); +} + +static void tc_dc(void) +{ + uint32_t now = 1000u; + uint8_t a[16]; + uint8_t body[1]; + uint16_t n; + cap_t c; + + reset_all(now); + handshake(now); + body[0] = 0x82u; /* S=1, DPC=ON */ + n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u); + feed_i(a, n, now); + CHECK((cap_i(0, &c) == 0) && (c.type == 52u) && (c.cot == IEC104_COT_ACTCON) + && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-013 选择段 actcon"); + CHECK((s_f_yk_sel == 1u) && (s_f_yk_ioa == 0x600Au), "TC-013 选择段转发源"); + body[0] = 0x02u; /* 执行 ON */ + n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u); + feed_i(a, n, now); + CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u) + && (s_f_yk_sel == 0u), "TC-013 执行段 actcon"); + /* 失配:选 ON 执 OFF */ + body[0] = 0x82u; + n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u); + feed_i(a, n, now); + body[0] = 0x01u; + n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u); + feed_i(a, n, now); + CHECK((cap_i(3, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u), + "TC-013 执行失配 PN"); + /* 拒绝类(YK1):选择段即 PN */ + body[0] = 0x82u; + n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x6000u, body, 1u); + feed_i(a, n, now); + CHECK((cap_i(4, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u), + "TC-013 无出口硬件 YK 选择段 PN"); +} + +/* TC-024(B7):YK12 全局软压板两段式转真(经真实板端源 + 原子封存) */ +static void tc_dc_yk12(void) +{ + uint32_t now = 1000u; + uint8_t a[16]; + uint8_t body[1]; + uint16_t n; + cap_t c; + + reset_all(now); + handshake(now); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 软压板默认投入"); + body[0] = 0x81u; /* S=1, DPC=OFF:选择段 */ + n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u); + feed_i(a, n, now); + CHECK((cap_i(0, &c) == 0) && (c.cot == IEC104_COT_ACTCON) + && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-024 YK12 选择段 actcon"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 选择段不动作"); + body[0] = 0x01u; /* 执行 OFF */ + n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u); + feed_i(a, n, now); + CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), + "TC-024 YK12 执行段 actcon"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-024 执行段软压板转真退出"); + CHECK(prot_set_selfcheck() == 0, "TC-024 set_sealed 原子封存 → selfcheck 通过"); + CHECK(prot_set_dirty_peek() == 1u, "TC-024 置脏待持久化"); + body[0] = 0x82u; /* 再投入:选择 ON */ + n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u); + feed_i(a, n, now); + body[0] = 0x02u; /* 执行 ON */ + n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u); + feed_i(a, n, now); + CHECK((cap_i(3, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), + "TC-024 投入执行 actcon"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 软压板转真投入"); + body[0] = 0x80u; /* DPC=00 非法:选择段即 PN */ + n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u); + feed_i(a, n, now); + CHECK((cap_i(4, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u), + "TC-024 非法 DPC 选择段 PN"); +} + +static void tc_se(void) +{ + uint32_t now = 1000u; + uint8_t a[24]; + uint8_t body[5]; + uint16_t n; + uint32_t addr = g_prot_set_meta[PS_OC1_CUR].comm_addr; /* 0x8858 */ + cap_t c; + + reset_all(now); + handshake(now); + CHECK(addr == 0x8858u, "TC-014 OC1 电流定值通讯地址"); + iec104_put_float(body, 5.0f); + body[4] = IEC104_QDC_S; /* 选择段 */ + n = mk_asdu(a, IEC104_T_C_SE_NC_1, 1u, IEC104_COT_ACT, 0u, 1u, addr, body, 5u); + feed_i(a, n, now); + CHECK((cap_i(0, &c) == 0) && (c.type == 50u) + && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-014 定值选择 actcon"); + body[4] = 0u; /* 执行段 */ + n = mk_asdu(a, IEC104_T_C_SE_NC_1, 1u, IEC104_COT_ACT, 0u, 1u, addr, body, 5u); + feed_i(a, n, now); + CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), + "TC-014 定值执行 actcon"); + CHECK(prot_set_get_f(PS_OC1_CUR) == 5.0f, "TC-014 定值已写入(真实 prot_set)"); + CHECK(prot_set_selfcheck() == 0, "TC-014 执行后 seal → selfcheck 通过"); + CHECK(prot_set_dirty_peek() == 1u, "TC-014 执行段置脏(B7 待落盘)"); + iec104_put_float(body, 100000.0f); /* 越上限 */ + body[4] = IEC104_QDC_S; + n = mk_asdu(a, IEC104_T_C_SE_NC_1, 1u, IEC104_COT_ACT, 0u, 1u, addr, body, 5u); + feed_i(a, n, now); + CHECK((cap_i(2, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u), + "TC-014 越界选择段 PN"); +} + +static void tc_rd(void) +{ + uint32_t now = 1000u; + uint8_t a[16]; + uint16_t n; + cap_t c; + + reset_all(now); + handshake(now); + s_f_yc[0] = 222.0f; + n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x4011u, NULL, 0u); + feed_i(a, n, now); + CHECK((cap_i(0, &c) == 0) && (c.type == 13u) && (c.cot == IEC104_COT_REQ) + && (iec104_get_float(c.obj) == 222.0f), "TC-015 C_RD YC 命中"); + n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x8858u, NULL, 0u); + feed_i(a, n, now); + CHECK((cap_i(1, &c) == 0) && (c.type == 13u) && (c.ioa == 0x8858u), + "TC-015 C_RD 定值命中"); + n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x9999u, NULL, 0u); + feed_i(a, n, now); + CHECK((cap_i(2, &c) == 0) && (c.type == 102u) + && (c.cot == IEC104_COT_UNK_IOA) && ((c.oa & 0x80u) != 0u), + "TC-015 未知 IOA COT14+EVI"); + n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0u, NULL, 0u); + feed_i(a, n, now); + CHECK((cap_i(3, &c) == 0) && (c.cot == IEC104_COT_UNK_IOA), + "TC-015 本地定值项(comm_addr=0)不可寻址(B7)"); +} + +static void tc_cs(void) +{ + uint32_t now = 1000u; + uint8_t a[24]; + uint8_t cp[7]; + uint16_t n; + cap_t c; + uint64_t e = 1709188496789ULL; + + reset_all(now); + handshake(now); + (void)iec104_epoch_ms_to_cp56(e, cp); + n = mk_asdu(a, IEC104_T_C_CS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, cp, 7u); + feed_i(a, n, now); + CHECK((cap_i(0, &c) == 0) && (c.type == 103u) + && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u) && (s_clock_ms == e), + "TC-016 C_CS 对时成功 epoch 精确"); + cp[2] |= 0x80u; /* IV */ + n = mk_asdu(a, IEC104_T_C_CS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, cp, 7u); + feed_i(a, n, now); + CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u), + "TC-016 CP56 IV → PN"); + (void)iec104_epoch_ms_to_cp56(e, cp); + s_clock_rc = -1; /* 系统侧拒绝 */ + n = mk_asdu(a, IEC104_T_C_CS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, cp, 7u); + feed_i(a, n, now); + CHECK((cap_i(2, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u), + "TC-016 clock_sync 失败 → PN"); +} + +static void tc_err_resp(void) +{ + uint32_t now = 1000u; + uint8_t a[16]; + uint16_t n, m; + cap_t c; + + reset_all(now); + handshake(now); + n = mk_asdu(a, 200u, 1u, IEC104_COT_ACT, 0u, 1u, 0u, NULL, 0u); + feed_i(a, n, now); + CHECK((cap_i(0, &c) == 0) && (c.type == 200u) && (c.cot == IEC104_COT_UNK_COT) + && ((c.oa & 0x80u) != 0u), "TC-017 未知类型 COT12+EVI"); + m = iec104_asdu_put_hdr(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 2u); + a[m] = IEC104_RQT_GROUP1; + m++; + feed_i(a, m, now); /* ca=2 ≠ 1 */ + CHECK((cap_i(1, &c) == 0) && (c.cot == IEC104_COT_UNK_CA) + && ((c.oa & 0x80u) != 0u), "TC-017 错公共地址 COT13+EVI"); +} + +static void tc_stopdt(void) +{ + uint32_t now = 1000u; + uint8_t a[16]; + uint8_t body[1]; + uint16_t n; + + reset_all(now); + handshake(now); + feed_u(0x13u, now); /* STOPDT act */ + CHECK((cap_u_count(0x23u) == 1) + && (iec104_link_state(&g_lk) == IEC104_ST_STOPPED), "TC-018 STOPDT con"); + body[0] = IEC104_RQT_GROUP1; + n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u); + feed_i(a, n, now); + CHECK(cap_i_count() == 0, "TC-018 STOPPED 态数据服务关"); + feed_u(0x43u, now); + CHECK(cap_u_count(0x83u) == 1, "TC-018 STOPPED 态 TESTFR 仍应答"); +} + +static void tc_seq_err(void) +{ + uint32_t now = 1000u; + uint8_t a[16]; + uint16_t n; + + reset_all(now); + handshake(now); + m_ns = 5u; /* 期望 0,投 5 */ + n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x4011u, NULL, 0u); + feed_i(a, n, now); + CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-019 序号错 → IDLE"); +} + +static void tc_pts(void) +{ + float v; + uint8_t q; + uint8_t d; + + reset_all(1000u); + CHECK((iec104_pts_yc_count() == 9u) && (iec104_pts_yx_count() == 33u) + && (iec104_pts_yc_ioa(8u) == 0x401Du), "TC-020 表尺寸/IOA"); + s_f_yc[0] = 7.5f; + CHECK((iec104_pts_yc_read(0u, &v, &q) == 0) && (v == 7.5f) && (q == 0u), + "TC-020 有效 YC"); + s_f_yc_rc[1] = -1; + (void)iec104_pts_yc_read(1u, &v, &q); + CHECK((v == 0.0f) && (q == IEC104_QDS_IV), "TC-020 无效通道 IV"); + s_f_yc_rc[2] = -2; + (void)iec104_pts_yc_read(2u, &v, &q); + CHECK(q == IEC104_QDS_NT, "TC-020 未就绪 NT"); + iec104_pts_yx_read(0u, &d); + CHECK(d == IEC104_DIQ_IV, "TC-020 无效遥信 IV"); + CHECK((iec104_pts_yx_is_dp(0u) == 1u) && (iec104_pts_yx_is_dp(4u) == 0u), + "TC-020 is_dp 分界"); + s_f_bits = (1uLL << 15); + iec104_pts_yx_read(15u, &d); + CHECK(d == 1u, "TC-020 有效遥信 ON"); +} + +static void tc_ts_rp(void) +{ + uint32_t now = 1000u; + uint8_t a[16]; + uint8_t body[2]; + uint16_t n; + cap_t c; + + reset_all(now); + handshake(now); + body[0] = 0u; + n = mk_asdu(a, IEC104_T_C_TS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u); + feed_i(a, n, now); + CHECK((cap_i(0, &c) == 0) && (c.type == 104u) && (c.cot == IEC104_COT_ACTCON), + "TC-021 C_TS actcon"); + n = mk_asdu(a, IEC104_T_C_RP_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u); + feed_i(a, n, now); + CHECK((cap_i(1, &c) == 0) && (c.type == 105u) && (c.cot == IEC104_COT_ACTCON), + "TC-021 C_RP actcon"); +} + +static void tc_periodic(void) +{ + uint32_t now = 1000u; + cap_t c; + + reset_all(now); + handshake(now); + iec104_link_poll(&g_lk, now + 60001u); + CHECK(cap_i_count() == 2, "TC-022 周期上送 2 分片"); + CHECK((cap_i(0, &c) == 0) && (c.type == 13u) && (c.cot == IEC104_COT_PERIODIC) + && (c.ioa == 0x4011u), "TC-022 周期 YC 组 COT=1"); + CHECK((cap_i(1, &c) == 0) && (c.ioa == 0x401Du), "TC-022 周期频率点"); +} + +static void tc_cov_kfull(void) +{ + uint32_t now = 1000u; + int i; + cap_t c; + + reset_all(now); + handshake(now); + iec104_link_poll(&g_lk, now); /* 先建快照(首 poll 只建不算变位) */ + for (i = 0; i < 12; i++) { + feed_rd(now); /* k 窗占满 */ + } + s_f_bits |= (1uLL << 14); /* YX15 变位 */ + iec104_link_poll(&g_lk, now + 100u); + CHECK(cap_i_count() == 12, "TC-023 k 满时自发发不出"); + feed_s(12u, now + 200u); /* 窗开 */ + iec104_link_poll(&g_lk, now + 300u); + CHECK((cap_i_count() == 13) && (cap_i(12, &c) == 0) && (c.ioa == 15u) + && (c.cot == IEC104_COT_SPONT), "TC-023 窗开后补发不丢事件"); +} + +int main(void) +{ + printf("=== IEC104 Protocol Stack Host Tests ===\n"); + tc_cp56(); + tc_ioa_float(); + tc_apci(); + tc_startdt(); + tc_open_ignore(); + tc_interrogation(); + tc_cov_yx(); + tc_cov_yc(); + tc_k_window(); + tc_w_thresh(); + tc_t2(); + tc_t3_t1(); + tc_dc(); + tc_dc_yk12(); + tc_se(); + tc_rd(); + tc_cs(); + tc_err_resp(); + tc_stopdt(); + tc_seq_err(); + tc_pts(); + tc_ts_rp(); + tc_periodic(); + tc_cov_kfull(); + printf("=== IEC104 Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/iec104/run_unit_test.py b/test/host/iec104/run_unit_test.py new file mode 100644 index 00000000..cf0979b7 --- /dev/null +++ b/test/host/iec104/run_unit_test.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""IEC 60870-5-104 被控站协议栈(B4)主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么:用本机 MSVC 离线编译 test/host/iec104/iec104_test.c(#include 纳入 + 生产源文件 iec104_asdu.c + iec104_pts.c + iec104_link.c,及数据源链接 + 依赖 prot_set.c + prot_elm.c + phasor.c + sample_ring.c),验证 + APCI 重组 / STARTDT 握手 / 总召分片 / COV / k-w-t1~t3 / 遥控与定值两段式 / + C_RD / C_CS / 错误应答(EVI)。 + +用法(仓库根目录): + & '.venv\\Scripts\\python.exe' test/host/iec104/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 + +设计依据:docs/子系统设计/IEC104上行设计说明.md §7(测试方案)。 +创建日期:2026-10-12 +""" +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="IEC104 Tests", + src=os.path.join(HERE, "iec104_test.c"), + include_dirs=[ + os.path.join(ROOT, "App", "algo"), + os.path.join(ROOT, "App", "protocol", "iec104"), + HERE, + ], + summary_marker="=== IEC104 Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/iec104_master.py b/test/host/iec104_master.py new file mode 100644 index 00000000..168a0d14 --- /dev/null +++ b/test/host/iec104_master.py @@ -0,0 +1,586 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""B6 bench leg: IEC 60870-5-104 master client (GI + COV round-trip). + +Task card: docs/任务规划/W1-采集保护收尾.md (B6 DoD); protocol baseline: +docs/子系统设计/IEC104上行设计说明.md §3/§4. Constants mirror +App/protocol/iec104/iec104_asdu.h / iec104_link.h. + +Flow: + 1. TCP connect device :2404, STARTDT act/con (enter RUN). + 2. General interrogation C_IC_NA_1 (QOI=20): expect actcon, background + scan (YX double-point group SQ=1 -> YX single-point group SQ=1 -> + YC group SQ=1 -> frequency single point), actterm. + 3. Deterministic COV trigger: bench analog inputs are floating (phasor + rms is 0..0.3 raw-code noise only; the standard source belongs to + B10), so we use the two-step C_SE_NC_1 write to temporarily lower + the OC1 current setting (comm addr 0x8858, default 10.0 A) to + 0.05 A -> protectTask (20 ms cadence) asserts OC1 -> YX16/YX15 + spontaneous (COT=3) uplink. This is exactly the "104 YX linkage" + leg left open by B5. Then restore 10.0 A -> YX16 returns. + 4. YK11 (0x600A) two-step select/execute -> positive actcon (reset + request consumed asynchronously by protectTask, B5 contract). + 5. C_RD read back 0x8858 ~= 10.0 (restore confirmation). + +Exit codes: 0 = all checks PASS; 1 = any check FAIL; 2 = connect or +protocol error. Output is pure ASCII (redirect stdout to archive the +round-trip log as bench evidence). + +Usage: python iec104_master.py [--ip 192.168.1.100] [--port 2404] + [--addr 0x8858] [--low 0.05] + [--restore 10.0] + +B7 bench leg modes (setpoint persistence + global soft plate, task card +docs/任务规划/W1-采集保护收尾.md B7; firmware side App/cfg/prot_set_store.c): + --b7-pre : before reset: mark setpoint 7.5 (C_SE+C_RD), YK12 OFF (two-step) + -> low setpoint 0.05 must NOT trip YX16 (plate OUT disables + protection); YK12 ON -> YX16 trips (linkage); restore 10.0; + leave plate OUT (persistence proof), wait for dirty->save. + --b7-post: after reset: C_RD 10.0 (setpoint persisted); low setpoint 0.05 + still must NOT trip (plate OUT persisted); YK12 ON -> YX16 + trips (linkage after reboot); restore 10.0, leave plate IN. + --b7-mark / --b7-check [--mark 7.5]: write a distinctive NON-default + setpoint (save), reset, read it back (literal "setpoint + hold" proof, distinguishable from factory default), restore. +""" +import argparse +import collections +import socket +import struct +import sys +import time + +# ---- constants (mirror iec104_asdu.h / iec104_link.h) ----------------------- +U_STARTDT_ACT, U_STARTDT_CON = 0x07, 0x0B +U_TESTFR_ACT, U_TESTFR_CON = 0x43, 0x83 + +T_M_SP_NA_1, T_M_DP_NA_1, T_M_ME_NC_1 = 1, 3, 13 +T_C_SE_NC_1, T_C_DC_NA_1 = 50, 52 +T_C_IC_NA_1, T_C_RD_NA_1 = 100, 102 + +COT_BG, COT_SPONT, COT_INIT, COT_REQ = 2, 3, 4, 5 +COT_ACT, COT_ACTCON, COT_ACTTERM = 6, 7, 8 +COT_PN = 0x40 # COT high byte bit6: negative confirm +RQT_GROUP1 = 20 +CA = 1 # common address fixed to 1 in V0.1 +SCAN_COTS = (COT_BG, COT_INIT) + +YX_NAMES = {15: "YX15 fault-total", 16: "YX16 OC-I", 17: "YX17 OC-II", + 18: "YX18 OC-III", 23: "YX23 overload", 31: "YX31 prot-block"} + +T0 = time.time() + + +def log(msg): + print("[%9.3fs] %s" % (time.time() - T0, msg), flush=True) + + +def hexs(b): + return " ".join("%02X" % x for x in b) + + +# ---- APCI / ASDU builders ---------------------------------------------------- +def u_frame(code): + return bytes([0x68, 0x04, code, 0x00, 0x00, 0x00]) + + +def s_frame(vr): + return bytes([0x68, 0x04, 0x01, 0x00, (vr << 1) & 0xFE, (vr >> 7) & 0x7F]) + + +def i_frame(vs, vr, body): + ctrl = bytes([(vs << 1) & 0xFE, (vs >> 7) & 0x7F, + (vr << 1) & 0xFE, (vr >> 7) & 0x7F]) + return bytes([0x68, len(body) + 4]) + ctrl + body + + +def put_ioa(ioa): + return bytes([ioa & 0xFF, (ioa >> 8) & 0xFF, (ioa >> 16) & 0xFF]) + + +def get_ioa(b, off=0): + return b[off] | (b[off + 1] << 8) | (b[off + 2] << 16) + + +def asdu(type_id, vsq, cot, cot_hi, ioa=None, payload=b""): + h = bytes([type_id, vsq, cot & 0x3F, cot_hi, CA & 0xFF, (CA >> 8) & 0xFF]) + if ioa is not None: + h += put_ioa(ioa) + return h + payload + + +class Master(object): + """Minimal 104 master. k/w/t are covered by "S after every recv batch" + (far faster than t1=15s / t2=10s), so no timers are needed here.""" + + def __init__(self, ip, port): + self.sock = socket.create_connection((ip, port), timeout=10) + self.sock.settimeout(1.0) + self.rx = b"" + self.vs = 0 + self.vr = 0 + self.vr_dirty = False + self.events = collections.deque() # (kind, detail) tuples + self.yx = {} # ioa -> (val, iv) + self.yc = {} # ioa -> (val, qds) + + # ---- send ---- + def _send(self, data, tag): + log("TX %s %s" % (tag, hexs(data))) + self.sock.sendall(data) + + def send_i(self, body, tag): + self._send(i_frame(self.vs, self.vr, body), tag) + self.vs = (self.vs + 1) & 0x7FFF + + def _send_s(self): + if self.vr_dirty: + self._send(s_frame(self.vr), "S N(R)=%d" % self.vr) + self.vr_dirty = False + + # ---- receive pump ---- + def pump_until(self, deadline): + """Pump RX until deadline (non-blocking semantics for wait_event).""" + while time.time() < deadline: + try: + chunk = self.sock.recv(512) + except socket.timeout: + self._send_s() + continue + if not chunk: + raise RuntimeError("peer closed (EOF)") + self.rx += chunk + while len(self.rx) >= 2 and self.rx[0] == 0x68: + ln = self.rx[1] + if ln < 4 or ln > 253: + raise RuntimeError("bad APDU length %d" % ln) + if len(self.rx) < 2 + ln: + break + apdu, self.rx = self.rx[:2 + ln], self.rx[2 + ln:] + self._handle(apdu) + if self.rx and self.rx[0] != 0x68: + raise RuntimeError("stream desync at 0x%02X" % self.rx[0]) + self._send_s() + + def wait_event(self, pred, timeout): + deadline = time.time() + timeout + while True: + for i, e in enumerate(self.events): + if pred(e): + return self.events[i] + if time.time() >= deadline: + return None + self.pump_until(min(deadline, time.time() + 1.0)) + + # ---- frame handling ---- + def _handle(self, apdu): + c = apdu[2:6] + b0, b1, b2, b3 = c[0], c[1], c[2], c[3] + if (b0 & 0x01) == 0: # I format + ns = (b0 >> 1) | ((b1 & 0x7F) << 7) + nr = (b2 >> 1) | ((b3 & 0x7F) << 7) + if ns != self.vr: + raise RuntimeError("N(S)=%d out of order (expect %d)" + % (ns, self.vr)) + self.vr = (self.vr + 1) & 0x7FFF + self.vr_dirty = True + self._asdu(apdu[6:], nr) + elif (b0 & 0x03) == 0x01: # S format + nr = (b2 >> 1) | ((b3 & 0x7F) << 7) + log("RX S N(R)=%d" % nr) + else: # U format + if b0 == U_STARTDT_CON: + log("RX U STARTDT con") + self.events.append(("startdt_con", None)) + elif b0 == U_TESTFR_ACT: + self._send(u_frame(U_TESTFR_CON), "U TESTFR con") + elif b0 == U_TESTFR_CON: + log("RX U TESTFR con") + else: + log("RX U code=0x%02X" % b0) + + def _asdu(self, a, nr): + if len(a) < 6: + raise RuntimeError("short ASDU") + type_id, vsq, cot, cot_hi = a[0], a[1], a[2], a[3] + pn = (cot_hi & COT_PN) != 0 + sq = (vsq & 0x80) != 0 + n = vsq & 0x7F + obj = a[6:] + + if type_id in (T_M_SP_NA_1, T_M_DP_NA_1): + pts = [] + if sq: + base = get_ioa(obj) + for i in range(n): + pts.append((base + i, obj[3 + i])) + else: + off = 0 + for _ in range(n): + pts.append((get_ioa(obj, off), obj[off + 3])) + off += 4 + for ioa, raw in pts: + val = (raw & 0x03) if type_id == T_M_DP_NA_1 else (raw & 0x01) + iv = (raw & 0x80) != 0 + self.yx[ioa] = (val, iv) + self.events.append(("yx", (ioa, val, iv, cot))) + if ioa in YX_NAMES or cot == COT_SPONT: + log("RX I %s ioa=%d val=%d iv=%d cot=%d [N(R)=%d]" + % ("M_DP" if type_id == T_M_DP_NA_1 else "M_SP", + ioa, val, 1 if iv else 0, cot, nr)) + elif type_id == T_M_ME_NC_1: + pts = [] + if sq: + base = get_ioa(obj) + for i in range(n): + v = struct.unpack_from(" second GI background scan") + gi_baseline(m, ck, "off-scan") + ck.add(name, m.yx.get(16) == (0, False), "via GI scan, yx16=%r" + % (m.yx.get(16),)) + + +def b7_pre(m, ck, args): + """B7 phase 1 (before reset): plate linkage + mark setpoint for save.""" + addr = int(args.addr, 0) + gi_baseline(m, ck, "pre") + v16 = m.yx.get(16) + ck.add("baseline_yx16_off", v16 is not None and v16 == (0, False), + "yx16=%r" % (v16,)) + + se_write(m, ck, addr, 7.5, "mark") + rd_set(m, ck, addr, 7.5, "rd_mark_7.5") + + yk_two_step(m, ck, 0x600B, 0x81, 0x01, "yk12_off") # plate OUT + se_write(m, ck, addr, args.low, "low") # 0.05 A + m.events.clear() + ck.add("plate_out_no_trip", m.wait_event(spont(16, 1), 3.0) is None, + "YX16 must stay OFF while plate OUT (protection disabled)") + rd_set(m, ck, addr, args.low, "rd_low_0.05") # setpoint still writable + + yk_two_step(m, ck, 0x600B, 0x82, 0x02, "yk12_on") # plate IN -> linkage + m.events.clear() + ck.add("spont_yx16_ON", m.wait_event(spont(16, 1), 5.0) is not None) + ck.add("spont_yx15_ON", m.wait_event(spont(15, 1), 3.0) is not None) + + se_write(m, ck, addr, args.restore, "restore") # back to 10.0 + yx16_off_confirmed(m, ck, "yx16_off_after_restore") + + yk_two_step(m, ck, 0x600B, 0x81, 0x01, "yk12_off_final") # leave OUT + log("wait 2s: dirty -> SYS_EVT_PROT_SET_SAVE -> /prot_set.bin") + time.sleep(2.0) + + +def b7_post(m, ck, args): + """B7 phase 2 (after reset): persistence + linkage after reboot.""" + addr = int(args.addr, 0) + gi_baseline(m, ck, "post") + v16 = m.yx.get(16) + ck.add("baseline_yx16_off", v16 is not None and v16 == (0, False), + "yx16=%r (plate OUT after reboot -> protection disabled)" % (v16,)) + + rd_set(m, ck, addr, args.restore, "persist_rd_10.0") # setpoint persisted + + se_write(m, ck, addr, args.low, "low") + m.events.clear() + ck.add("plate_out_persisted_no_trip", + m.wait_event(spont(16, 1), 3.0) is None, + "plate OUT state survived the reset") + + yk_two_step(m, ck, 0x600B, 0x82, 0x02, "yk12_on") # linkage after reboot + m.events.clear() + ck.add("spont_yx16_ON_after_reboot", + m.wait_event(spont(16, 1), 5.0) is not None) + + se_write(m, ck, addr, args.restore, "restore") + yx16_off_confirmed(m, ck, "yx16_off_final") + log("wait 1s: final dirty -> save, leave plate IN") + time.sleep(1.0) + + +def b7_mark(m, ck, args): + """B7 distinctive-value mark: write non-default setpoint, let it save.""" + addr = int(args.addr, 0) + se_write(m, ck, addr, args.mark, "distinct") + rd_set(m, ck, addr, args.mark, "rd_distinct") + log("wait 1.5s: dirty -> save (/prot_set.bin)") + time.sleep(1.5) + + +def b7_check(m, ck, args): + """B7 distinctive-value check after reset: must read back non-default, + then restore default so the bench is left in a clean state.""" + addr = int(args.addr, 0) + rd_set(m, ck, addr, args.mark, "persist_distinct_after_reset") + se_write(m, ck, addr, args.restore, "restore") + log("wait 1s: restore saved, bench left at defaults") + time.sleep(1.0) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument("--ip", default="192.168.1.100") + ap.add_argument("--port", type=int, default=2404) + ap.add_argument("--addr", default="0x8858", + help="setpoint comm addr to drive COV (OC1 current)") + ap.add_argument("--low", type=float, default=0.05, + help="temporary low OC1 setting (A) to force pickup") + ap.add_argument("--restore", type=float, default=10.0, + help="OC1 default setting to restore (A)") + ap.add_argument("--b7-pre", action="store_true", + help="B7 phase 1 (before reset) instead of B6 flow") + ap.add_argument("--b7-post", action="store_true", + help="B7 phase 2 (after reset) instead of B6 flow") + ap.add_argument("--b7-mark", action="store_true", + help="B7 write distinctive non-default setpoint (pre-reset)") + ap.add_argument("--b7-check", action="store_true", + help="B7 verify distinctive setpoint survived reset, restore") + ap.add_argument("--mark", type=float, default=7.5, + help="distinctive setpoint value (A), must be non-default") + args = ap.parse_args() + addr = int(args.addr, 0) + b7_mode = ("pre" if args.b7_pre else "post" if args.b7_post else + "mark" if args.b7_mark else "check" if args.b7_check else None) + + ck = Checks() + log("iec104 master client -> %s:%d mode=%s" + % (args.ip, args.port, b7_mode or "b6")) + + try: + m = Master(args.ip, args.port) + except Exception as e: + log("CONNECT FAIL: %r" % (e,)) + return 2 + + rc = 2 + try: + # 1. STARTDT + m._send(u_frame(U_STARTDT_ACT), "U STARTDT act") + ck.add("startdt_con", m.wait_event( + lambda e: e[0] == "startdt_con", 5.0) is not None) + + if b7_mode is not None: + if b7_mode == "pre": + b7_pre(m, ck, args) + elif b7_mode == "post": + b7_post(m, ck, args) + elif b7_mode == "mark": + b7_mark(m, ck, args) + else: + b7_check(m, ck, args) + m.close() + rc = 0 if all(ok for _, ok, _ in ck.items) else 1 + n_ok = sum(1 for _, ok, _ in ck.items) + log("SUMMARY: %d/%d checks PASS -> %s" + % (n_ok, len(ck.items), "PASS" if rc == 0 else "FAIL")) + return rc + + # 2. first general interrogation + m.send_i(asdu(T_C_IC_NA_1, 1, COT_ACT, 0, ioa=0, + payload=bytes([RQT_GROUP1])), + "I C_IC act (QOI=20)") + ck.add("gi_actcon", m.wait_event(actcon_of(T_C_IC_NA_1, False), + 5.0) is not None) + + def scan_yx(ioa): + return lambda e: (e[0] == "yx" and e[1][0] == ioa + and e[1][3] in SCAN_COTS) + + ck.add("scan_yx_dp(ioa4)", m.wait_event(scan_yx(4), 10.0) is not None) + ck.add("scan_yx_sp(ioa16)", m.wait_event(scan_yx(16), 10.0) is not None) + ck.add("scan_yc(ioa0x4018)", m.wait_event( + lambda e: (e[0] == "yc" and e[1][0] == 0x4018 + and e[1][3] in SCAN_COTS), 10.0) is not None) + ck.add("scan_freq(ioa0x401D)", m.wait_event( + lambda e: (e[0] == "yc" and e[1][0] == 0x401D + and e[1][3] in SCAN_COTS), 10.0) is not None) + ck.add("gi_actterm", m.wait_event( + lambda e: (e[0] == "asdu" and e[1][0] == T_C_IC_NA_1 + and e[1][1] == COT_ACTTERM), 10.0) is not None) + + v16 = m.yx.get(16) + ck.add("baseline_yx16_off", v16 is not None and v16 == (0, False), + "yx16=%r" % (v16,)) + + # 3. COV up: lower OC1 setting (two-step C_SE_NC_1) + def se(select): + pl = struct.pack(" second GI background scan") + m.send_i(asdu(T_C_IC_NA_1, 1, COT_ACT, 0, ioa=0, + payload=bytes([RQT_GROUP1])), + "I C_IC act #2 (QOI=20)") + ok_term = m.wait_event( + lambda e: (e[0] == "asdu" and e[1][0] == T_C_IC_NA_1 + and e[1][1] == COT_ACTTERM), 15.0) is not None + ck.add("yx16_OFF", ok_term and m.yx.get(16) == (0, False), + "via 2nd GI scan, yx16=%r" % (m.yx.get(16),)) + + # 5. YK11 two-step (positive confirm; reset consumed by protectTask) + m.send_i(asdu(T_C_DC_NA_1, 1, COT_ACT, 0, ioa=0x600A, + payload=bytes([0x82])), "I C_DC YK11 select (QDC=82H)") + ck.add("yk11_select_con", m.wait_event(actcon_of(T_C_DC_NA_1, False), + 5.0) is not None) + m.send_i(asdu(T_C_DC_NA_1, 1, COT_ACT, 0, ioa=0x600A, + payload=bytes([0x02])), "I C_DC YK11 execute (QDC=02H)") + ck.add("yk11_execute_con", m.wait_event(actcon_of(T_C_DC_NA_1, False), + 5.0) is not None) + + # 6. C_RD read back the restored setpoint + m.send_i(asdu(T_C_RD_NA_1, 1, COT_REQ, 0, ioa=addr), + "I C_RD 0x%X" % addr) + e = m.wait_event(lambda e: (e[0] == "yc" and e[1][0] == addr + and e[1][3] == COT_REQ), 5.0) + ck.add("rd_setpoint_10.0", + e is not None and abs(e[1][1] - args.restore) < 0.1, + "read=%.3f" % (e[1][1],) if e else "no response") + + m.close() + rc = 0 if all(ok for _, ok, _ in ck.items) else 1 + except Exception as e: + log("PROTOCOL ERROR: %r" % (e,)) + rc = 2 + + n_ok = sum(1 for _, ok, _ in ck.items) + log("SUMMARY: %d/%d checks PASS -> %s" + % (n_ok, len(ck.items), "PASS" if rc == 0 else ("FAIL" if rc == 1 else "ERROR"))) + return rc + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/host/n30_repro.py b/test/host/n30_repro.py new file mode 100644 index 00000000..a82db06e --- /dev/null +++ b/test/host/n30_repro.py @@ -0,0 +1,74 @@ +# N-30 确定性复现:连续快速 STOR(无间隔),统计 425/超时失败次数 +# 用法: python n30_repro.py [--host IP] [--user U --pwd P] [--n 10] [--size 1024] +# 退出码: 0=全部成功 / 1=有失败 / 2=连接错误 +import argparse +import io +import sys +from ftplib import FTP, error_temp, error_perm + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--host", default="192.168.1.100") + ap.add_argument("--port", type=int, default=21) + ap.add_argument("--user", default=None, help="None=anonymous") + ap.add_argument("--pwd", default=None) + ap.add_argument("--n", type=int, default=10) + ap.add_argument("--size", type=int, default=1024) + args = ap.parse_args() + + data = b"X" * args.size + fails = [] + try: + f = FTP() + f.connect(args.host, args.port, timeout=15) + if args.user is None: + f.login("anonymous", "x@x") + else: + f.login(args.user, args.pwd) + f.voidcmd("TYPE I") + for i in range(args.n): + name = "n30_%02d.tmp" % i + try: + with io.BytesIO(data) as b: + f.storbinary("STOR " + name, b) + print("STOR #%02d OK" % (i + 1)) + except (error_temp, error_perm, OSError, EOFError) as e: + print("STOR #%02d FAIL %r" % (i + 1, e)) + fails.append(i + 1) + # 会话可能已重启:尝试重登继续计数 + try: + f.quit() + except Exception: + pass + try: + f = FTP() + f.connect(args.host, args.port, timeout=15) + if args.user is None: + f.login("anonymous", "x@x") + else: + f.login(args.user, args.pwd) + f.voidcmd("TYPE I") + except Exception as e2: + print("re-login failed: %r" % e2) + break + # 清理 + try: + for i in range(args.n): + try: + f.delete("n30_%02d.tmp" % i) + except Exception: + pass + f.quit() + except Exception: + pass + except Exception as e: + print("connect error: %r" % e) + return 2 + + print("RESULT: %d/%d failed %s" % (len(fails), args.n, fails)) + return 1 if fails else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/host/phasor/phasor_test.c b/test/host/phasor/phasor_test.c new file mode 100644 index 00000000..0a14e669 --- /dev/null +++ b/test/host/phasor/phasor_test.c @@ -0,0 +1,553 @@ +/* + * 模块名称:相量层(L2)主机侧单元测试 + * 模块功能:在 PC 上对 App/algo/phasor.c + App/algo/sample_ring.c 做离线验证。 + * + * 被测对象:真实源文件(以 #include 原样纳入,保证测的是生产代码而非副本): + * ../../../App/algo/phasor.c + * ../../../App/algo/sample_ring.c + * 本层为**纯算法**(无寄存器、无 RTOS、无文件/外设依赖),故**无需任何桩**。 + * + * 为什么要主机侧单测(而不是只用板端套件): + * 1. 相量算法是纯数学变换,与硬件无关;板端**无信号源**,无法注入已知幅值/相位/ + * 频率的标准波形,因而无法验证"读数 = 设定值"这一核心判据(同 L1-3 的替代口径)。 + * 主机侧可合成任意波形,把幅值精度/谐波抑制/频率敏感性一次性钉死。 + * 2. 可精确构造边界:恰好一周波 / 不足一周波 / 环形回绕 / 任意次谐波 / 频偏。 + * 3. 不占 MCU 的 RAM,毫秒级完成。 + * + * 覆盖点(对应 docs/算法/继电保护功能复现规划.md §3.2): + * TC-PH-001 L2-1 系数表逐项等于 sin(2πk/N)·(√2/N) / cos(2πk/N)·(√2/N)(防转录错) + * TC-PH-002 L2-1 系数表精确零点(sin@0,10 / cos@5,15)与归一化 Σcoe² = 1/N + * TC-PH-003 L2-3 纯基波:fAmSQ = A/√2(有效值),fAm = √2·fAmSQ(幅值) + * TC-PH-004 L2-3/L2-4 实部虚部与相位:atan2(fIm,fRe) = 注入相角 + * TC-PH-005 直流 + 2/3/5 次谐波被完全滤除(基波读数不变) + * TC-PH-006 任意次谐波提取(索引步进 = 谐波次数):k=2 / k=5 + * TC-PH-007 频率偏离 50 Hz 的幅值误差(±1 Hz ≤ 1.5%,±2 Hz ≤ 3%,见 §9 Q3) + * TC-PH-010 采样环:入环/取窗时序、回绕、饱和、帧计数、参数守卫 + * TC-PH-020 L2-2 实例层:实例数/实例表/未攒满一周波不产出 + * TC-PH-021 L2-6 滑动窗:连续滑动 20 拍基波读数恒定 + * TC-PH-022 L2-7 双区交换:使用区交替 + get 返回**最后一次** run 的结果(非陈旧) + * TC-PH-030..034 phasor_zero 四字段清零 / phasor_init 双区清零 + 使用区与就绪标志复位 + 幂等 + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-10 + */ + +#include +#include +#include + +#define PH_PI 3.14159265358979323846 +/* 采样率(Hz):1000 Hz @ 50 Hz → 20 点/周波 */ +#define PH_FS 1000.0 +#define PH_F0 50.0 + +/* ===================== 断言 ===================== */ +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 "sample_ring.c" +#include "phasor.c" + +/* ===================== 工具 ===================== */ + +/* + * 函数功能:生成一段正弦窗 + * 入口参数:x - 输出(长度 >= n) + * n - 点数 + * amp - 正弦峰值 + * f - 频率(Hz) + * phase - 初相(度) + * 返 回 值:无 + * 限定条件:无 + * 函数说明:以 t = i/PH_FS 采样,即"以当前时刻为终止的前 n 点窗"。 + */ +static void gen_sine(float *x, uint8_t n, double amp, double f, double phase) { + uint8_t i; + + for (i = 0u; i < n; i++) { + x[i] = (float)(amp * sin(2.0 * PH_PI * f * (double)i / PH_FS + phase * PH_PI / 180.0)); + } +} + +/* + * 函数功能:按时刻 t 构造一整帧(9 路语义通道同值) + * 入口参数:f - 输出帧(长度 = ADC_SEM_COUNT) + * t - 采样序号(t/PH_FS 秒) + * amp - 正弦峰值 + * phase - 初相(度) + * 返 回 值:无 + * 限定条件:无 + * 函数说明:仅用于驱动实例层/采样环;**不代表真实硬件**(V0.1 上各通道不同刻, + * 此处只为验证相量层本身)。 + */ +static void frame_at(float *f, uint32_t t, double amp, double phase) { + uint8_t c; + double v = amp * sin(2.0 * PH_PI * PH_F0 * (double)t / PH_FS + phase * PH_PI / 180.0); + + for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) { + f[c] = (float)v; + } +} + +/* ===================== TC-PH-001/002:L2-1 系数表 ===================== */ + +static void tc_table(void) { + const double scale = sqrt(2.0) / (double)PHASOR_N; + double sum_sin = 0.0, sum_cos = 0.0; + int ok_sin = 1, ok_cos = 1, ok_norm = 1; + uint8_t k; + + for (k = 0u; k < (uint8_t)PHASOR_N; k++) { + double arg = 2.0 * PH_PI * (double)k / (double)PHASOR_N; + double es = sin(arg) * scale; + double ec = cos(arg) * scale; + + if (fabs((double)k_fft_sin[k] - es) > 1e-7) { ok_sin = 0; } + if (fabs((double)k_fft_cos[k] - ec) > 1e-7) { ok_cos = 0; } + + /* 归一化:sin²+cos² = (√2/N)² = 2/N² */ + if (fabs((double)k_fft_sin[k] * (double)k_fft_sin[k] + + (double)k_fft_cos[k] * (double)k_fft_cos[k] - 2.0 / (PHASOR_N * PHASOR_N)) > 1e-9) { + ok_norm = 0; + } + sum_sin += (double)k_fft_sin[k] * (double)k_fft_sin[k]; + sum_cos += (double)k_fft_cos[k] * (double)k_fft_cos[k]; + } + + CHECK(ok_sin, "L2-1 coeff sin[k] == sin(2*pi*k/N)*(sqrt2/N) for all k"); + CHECK(ok_cos, "L2-1 coeff cos[k] == cos(2*pi*k/N)*(sqrt2/N) for all k"); + CHECK(ok_norm, "L2-1 per-k sin^2+cos^2 == 2/N^2 (normalization)"); + /* Σsin² = Σcos² = (√2/N)²·(N/2) = 1/N:这就是"fAmSQ 直接是有效值"的根 */ + CHECK(fabs(sum_sin - 1.0 / PHASOR_N) < 1e-7, "L2-1 sum(sin^2) == 1/N (=> RMS, not amplitude)"); + CHECK(fabs(sum_cos - 1.0 / PHASOR_N) < 1e-7, "L2-1 sum(cos^2) == 1/N (=> RMS, not amplitude)"); + + CHECK((k_fft_sin[0] == 0.0f) && (k_fft_sin[10] == 0.0f), "L2-1 sin exact zeros at k=0,10"); + CHECK((k_fft_cos[5] == 0.0f) && (k_fft_cos[15] == 0.0f), "L2-1 cos exact zeros at k=5,15"); + CHECK(fabs((double)k_fft_sin[5] - sqrt(2.0) / PHASOR_N) < 1e-7, + "L2-1 sin[5] == sqrt(2)/N (peak coefficient)"); +} + +/* ===================== TC-PH-003/004:基波幅值 / 有效值 / 相位 ===================== */ + +static void tc_fundamental(void) { + static const struct { double amp; double ph; } k_case[] = { + { 57.7, 0.0 }, + { 57.7, 30.0 }, + { 57.7, -45.0 }, + { 100.0, 90.0 }, + { 1.0, 0.0 }, + { 32767.0, 137.0 }, + }; + const unsigned int ncase = (unsigned int)(sizeof(k_case) / sizeof(k_case[0])); + unsigned int i; + int ok_rms = 1, ok_amp = 1, ok_ph = 1; + float x[PHASOR_N]; + + for (i = 0u; i < ncase; i++) { + phasor_t p; + double exp_rms = k_case[i].amp / sqrt(2.0); + double ph_meas; + + gen_sine(x, (uint8_t)PHASOR_N, k_case[i].amp, PH_F0, k_case[i].ph); + phasor_calc(x, (uint8_t)PHASOR_N, 1u, &p); + + if (fabs((double)p.fAmSQ - exp_rms) / exp_rms > 0.01) { ok_rms = 0; } + if (fabs((double)p.fAm - exp_rms * sqrt(2.0)) / (exp_rms * sqrt(2.0)) > 0.01) { ok_amp = 0; } + + ph_meas = atan2((double)p.fIm, (double)p.fRe) * 180.0 / PH_PI; + if (fabs(ph_meas - k_case[i].ph) > 0.5) { + printf(" (amp=%.1f ph=%.1f -> measured %.4f)\n", + k_case[i].amp, k_case[i].ph, ph_meas); + ok_ph = 0; + } + } + + CHECK(ok_rms, "L2-3 pure fundamental: fAmSQ == A/sqrt(2) (RMS) within 1%"); + CHECK(ok_amp, "L2-3 fAm == sqrt(2)*fAmSQ (amplitude) within 1%"); + CHECK(ok_ph, "L2-4/L2-3 atan2(fIm,fRe) == injected phase (6 amp/phase cases)"); +} + +/* ===================== TC-PH-005:直流与谐波被滤除 ===================== */ + +static void tc_filtering(void) { + const double amp = 57.7, exp_rms = amp / sqrt(2.0); + static const struct { double dc, h2, h3, h5; const char *name; } k_case[] = { + { 0.0, 0.0, 0.0, 0.0, "pure" }, + { 1.0, 0.0, 0.0, 0.0, "with 1V DC" }, + { 0.0, 0.0, 30.0, 0.0, "with 30% 3rd harmonic" }, + { 0.0, 0.0, 0.0, 11.5, "with 20% 5th harmonic" }, + { 1.0, 20.0, 15.0, 11.5, "DC + 2nd + 3rd + 5th (doc 6 case)" }, + }; + const unsigned int ncase = (unsigned int)(sizeof(k_case) / sizeof(k_case[0])); + unsigned int i; + int ok = 1; + float x[PHASOR_N]; + + for (i = 0u; i < ncase; i++) { + phasor_t p; + uint8_t k; + double err; + + for (k = 0u; k < (uint8_t)PHASOR_N; k++) { + double arg = 2.0 * PH_PI * (double)k / (double)PHASOR_N; + x[k] = (float)(amp * sin(arg + 30.0 * PH_PI / 180.0) + + k_case[i].dc + + k_case[i].h2 * sin(2.0 * arg) + + k_case[i].h3 * sin(3.0 * arg) + + k_case[i].h5 * sin(5.0 * arg)); + } + + phasor_calc(x, (uint8_t)PHASOR_N, 1u, &p); + err = fabs((double)p.fAmSQ - exp_rms) / exp_rms; + if (err > 0.01) { + printf(" (%s -> fAmSQ=%.4f expect=%.4f err=%.3f%%)\n", + k_case[i].name, (double)p.fAmSQ, exp_rms, err * 100.0); + ok = 0; + } + } + + CHECK(ok, "L2-3 DC + 2nd/3rd/5th harmonics fully rejected (5 cases, err <= 1%)"); +} + +/* ===================== TC-PH-006:任意次谐波提取 ===================== */ + +static void tc_harmonic(void) { + /* 基波 100 + 二次 25 + 五次 12(峰值);三根谱线应各自独立读出 */ + static const struct { uint8_t harm; double amp; } k_case[] = { + { 1u, 100.0 }, + { 2u, 25.0 }, + { 5u, 12.0 }, + }; + const unsigned int ncase = (unsigned int)(sizeof(k_case) / sizeof(k_case[0])); + unsigned int i; + int ok = 1; + float x[PHASOR_N]; + uint8_t k; + + for (k = 0u; k < (uint8_t)PHASOR_N; k++) { + double arg = 2.0 * PH_PI * (double)k / (double)PHASOR_N; + x[k] = (float)(100.0 * sin(arg) + 25.0 * sin(2.0 * arg) + 12.0 * sin(5.0 * arg)); + } + + for (i = 0u; i < ncase; i++) { + phasor_t p; + double exp_rms = k_case[i].amp / sqrt(2.0); + + phasor_calc(x, (uint8_t)PHASOR_N, k_case[i].harm, &p); + if (fabs((double)p.fAmSQ - exp_rms) / exp_rms > 0.01) { + printf(" (harm=%u -> fAmSQ=%.4f expect=%.4f)\n", + (unsigned)k_case[i].harm, (double)p.fAmSQ, exp_rms); + ok = 0; + } + } + + CHECK(ok, "L2-1 harmonic extraction k=1/2/5 from a mixed signal (index step = order)"); + + /* 纯基波时谐波轨应≈0(选择性) */ + gen_sine(x, (uint8_t)PHASOR_N, 100.0, PH_F0, 0.0); + { + phasor_t p2, p5; + phasor_calc(x, (uint8_t)PHASOR_N, 2u, &p2); + phasor_calc(x, (uint8_t)PHASOR_N, 5u, &p5); + CHECK(((double)p2.fAmSQ < 1e-3) && ((double)p5.fAmSQ < 1e-3), + "L2-1 pure fundamental leaves 2nd/5th harmonic bins ~0"); + } +} + +/* ===================== TC-PH-007:频率敏感性 ===================== */ + +static void tc_freqdev(void) { + const double amp = 57.7, exp_rms = amp / sqrt(2.0); + /* 最坏相角下的幅值误差(实测:±1 Hz ≤1.1%,±2 Hz ≤2.33%;口径见 §9 Q3) */ + static const struct { double f; double limit_pct; } k_case[] = { + { 50.0, 0.10 }, + { 49.0, 1.50 }, + { 51.0, 1.50 }, + { 48.0, 3.00 }, + { 52.0, 3.00 }, + }; + const unsigned int ncase = (unsigned int)(sizeof(k_case) / sizeof(k_case[0])); + unsigned int i; + int ok = 1; + float x[PHASOR_N]; + + for (i = 0u; i < ncase; i++) { + double worst = 0.0; + int ph; + + for (ph = 0; ph < 360; ph += 15) { + phasor_t p; + double err; + + gen_sine(x, (uint8_t)PHASOR_N, amp, k_case[i].f, (double)ph); + phasor_calc(x, (uint8_t)PHASOR_N, 1u, &p); + err = fabs((double)p.fAmSQ - exp_rms) / exp_rms * 100.0; + if (err > worst) { worst = err; } + } + + printf(" (f=%5.1f Hz worst err over phase = %.2f%%, limit %.2f%%)\n", + k_case[i].f, worst, k_case[i].limit_pct); + if (worst > k_case[i].limit_pct) { ok = 0; } + } + + CHECK(ok, "L2-3 frequency sensitivity: 50Hz<=0.1%, +-1Hz<=1.5%, +-2Hz<=3% (worst phase)"); +} + +/* ===================== TC-PH-010:采样环 ===================== */ + +static void tc_ring(void) { + float f[ADC_SEM_COUNT]; + float w[SAMPLE_RING_DEPTH]; + uint8_t c; + int i; + int ok; + + sample_ring_init(); + CHECK(sample_ring_filled() == 0u, "L2-in ring: init clears fill"); + CHECK(sample_ring_frames() == 0u, "L2-in ring: init clears frame counter"); + CHECK(sample_ring_window(ADC_SEM_UA, 20u, w) == -2, "L2-in ring: window before data -> -2"); + + /* 前 3 帧:UA = 100/200/300 */ + ok = 1; + for (i = 0; i < 3; i++) { + for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) { f[c] = 0.0f; } + f[ADC_SEM_UA] = (float)(100 * (i + 1)); + if (sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT) != 0) { ok = 0; } + } + CHECK(ok, "L2-in ring: 3 frames pushed OK"); + CHECK(sample_ring_filled() == 3u, "L2-in ring: filled == 3"); + CHECK((sample_ring_window(ADC_SEM_UA, 3u, w) == 0) + && (w[0] == 100.0f) && (w[1] == 200.0f) && (w[2] == 300.0f), + "L2-in ring: window is chronological (old -> new)"); + /* 通道隔离:其它语义通道不应被 UA 的写入污染 */ + CHECK((sample_ring_window(ADC_SEM_IB, 3u, w) == 0) + && (w[0] == 0.0f) && (w[1] == 0.0f) && (w[2] == 0.0f), + "L2-in ring: channels are independent"); + + /* 推到 45 帧(> DEPTH=40)→ 回绕 */ + for (i = 3; i < 45; i++) { + for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) { f[c] = 0.0f; } + f[ADC_SEM_UA] = (float)i; + (void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT); + } + CHECK(sample_ring_filled() == SAMPLE_RING_DEPTH, "L2-in ring: fill saturates at DEPTH"); + CHECK(sample_ring_frames() == 45u, "L2-in ring: frame counter is NOT saturated"); + + ok = (sample_ring_window(ADC_SEM_UA, 20u, w) == 0); + for (i = 0; i < 20; i++) { if (w[i] != (float)(25 + i)) { ok = 0; } } + CHECK(ok, "L2-in ring: wrapped window == last 20 values, in order"); + + ok = (sample_ring_window(ADC_SEM_UA, (uint8_t)SAMPLE_RING_DEPTH, w) == 0); + for (i = 0; i < (int)SAMPLE_RING_DEPTH; i++) { if (w[i] != (float)(5 + i)) { ok = 0; } } + CHECK(ok, "L2-in ring: full-depth window == last DEPTH values, in order"); + + CHECK(sample_ring_window(ADC_SEM_UA, 0u, w) == -1, "L2-in ring: n=0 rejected"); + CHECK(sample_ring_window(ADC_SEM_UA, (uint8_t)(SAMPLE_RING_DEPTH + 1u), w) == -1, + "L2-in ring: n > DEPTH rejected"); + CHECK(sample_ring_window((adc_semantic_t)ADC_SEM_COUNT, 20u, w) == -1, + "L2-in ring: out-of-range channel rejected"); + CHECK(sample_ring_window(ADC_SEM_UA, 20u, NULL) == -1, "L2-in ring: NULL out rejected"); + CHECK(sample_ring_push_frame(NULL, (uint8_t)ADC_SEM_COUNT) == -1, "L2-in ring: NULL frame rejected"); + CHECK(sample_ring_push_frame(f, 3u) == -1, "L2-in ring: wrong frame length rejected"); +} + +/* ===================== TC-PH-020/021/022:实例层 + 滑动窗 + 双区 ===================== */ + +static void tc_instances(void) { + const double amp = 100.0, exp_rms = amp / sqrt(2.0); + float f[ADC_SEM_COUNT]; + phasor_t p; + uint32_t t; + int ok; + uint8_t i; + + CHECK(phasor_inst_count() == 5u, "L2-2 instance count == 5"); + CHECK((phasor_inst(0u) != NULL) && (phasor_inst(0u)->name != NULL), "L2-2 instance name available"); + CHECK(phasor_inst((uint8_t)phasor_inst_count()) == NULL, "L2-2 out-of-range instance -> NULL"); + + phasor_init(); + CHECK(s_use_zone == 0u, "L2-7 use zone starts at 0"); + CHECK(phasor_get(0u, &p) == -2, "L2-2 no result before a full cycle"); + CHECK(phasor_get((uint8_t)phasor_inst_count(), &p) == -1, "L2-2 out-of-range index -> -1"); + CHECK(phasor_get(0u, NULL) == -1, "L2-2 NULL out -> -1"); + + /* 喂 19 帧(不足一周波)→ 仍不产出 */ + for (t = 0u; t < 19u; t++) { + frame_at(f, t, amp, 0.0); + (void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT); + phasor_run(); + } + CHECK(phasor_get(0u, &p) == -2, "L2-2 still no result after 19 frames"); + CHECK(s_use_zone == 0u, "L2-7 use zone unchanged while not producing"); + + /* 第 20 帧 → 攒满一周波,产出 */ + frame_at(f, 19u, amp, 0.0); + (void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT); + phasor_run(); + CHECK(phasor_get(0u, &p) == 0, "L2-2 result available after 20 frames"); + CHECK(s_use_zone == 1u, "L2-7 use zone toggles to 1 after first production"); + CHECK(fabs((double)p.fAmSQ - exp_rms) / exp_rms < 0.01, + "L2-2/L2-3 instance 0 (C01-IA-F1) RMS == A/sqrt(2)"); + { + phasor_t p2, p5; + int ok_sel = 1; + if (phasor_get(3u, &p2) != 0) { ok_sel = 0; } + if (phasor_get(4u, &p5) != 0) { ok_sel = 0; } + CHECK(ok_sel && ((double)p2.fAmSQ < 1e-3) && ((double)p5.fAmSQ < 1e-3), + "L2-2 harmonic instances (C04-IA-F2 / C05-IA-F5) ~0 on pure fundamental"); + } + + /* L2-6 滑动窗:继续推 20 帧,每拍重算,基波读数应恒定 */ + ok = 1; + for (t = 20u; t < 40u; t++) { + frame_at(f, t, amp, 0.0); + (void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT); + phasor_run(); + if (phasor_get(0u, &p) != 0) { ok = 0; } + else if (fabs((double)p.fAmSQ - exp_rms) / exp_rms > 0.01) { ok = 0; } + } + CHECK(ok, "L2-6 sliding window: RMS constant across 20 consecutive slides"); + + /* 实例表一致性:逐实例复核"实例窗 == 独立 phasor_calc 结果" */ + ok = 1; + for (i = 0u; i < phasor_inst_count(); i++) { + const phasor_inst_t *d = phasor_inst(i); + float win[PHASOR_N]; + phasor_t ref, got; + + if (d == NULL) { ok = 0; break; } + if (sample_ring_window(d->sem, (uint8_t)PHASOR_N, win) != 0) { ok = 0; break; } + phasor_calc(win, (uint8_t)PHASOR_N, d->harm, &ref); + if (phasor_get(i, &got) != 0) { ok = 0; break; } + if ((ref.fRe != got.fRe) || (ref.fIm != got.fIm) || (ref.fAmSQ != got.fAmSQ)) { ok = 0; break; } + } + CHECK(ok, "L2-2/L2-6 instance results == phasor_calc() on the same ring window"); +} + +/* ===================== TC-PH-030..034:phasor_zero / init 清零 ===================== */ + +static void tc_zero(void) { + phasor_t p; + uint8_t i; + int zones_dirty = 0; + + /* 1) 直接白盒:phasor_zero 把四个字段(含 fAm / fAmSQ)全部清零 */ + p.fRe = 1.0f; p.fIm = -2.0f; p.fAm = 3.0f; p.fAmSQ = 4.0f; + phasor_zero(&p); + CHECK((p.fRe == 0.0f) && (p.fIm == 0.0f) && (p.fAm == 0.0f) && (p.fAmSQ == 0.0f), + "TC-PH-030 phasor_zero clears all four fields"); + + /* 2) 经 phasor_init:先把双区写成脏值,init 后必须全零,使用区复位、就绪标志清 */ + for (i = 0u; i < (uint8_t)PHASOR_INST_N; i++) { + s_zone[0][i].fRe = 9.0f; s_zone[0][i].fIm = 9.0f; + s_zone[0][i].fAm = 9.0f; s_zone[0][i].fAmSQ = 9.0f; + s_zone[1][i].fRe = 9.0f; s_zone[1][i].fIm = 9.0f; + s_zone[1][i].fAm = 9.0f; s_zone[1][i].fAmSQ = 9.0f; + } + s_use_zone = 1u; + (void)phasor_init(); + for (i = 0u; i < (uint8_t)PHASOR_INST_N; i++) { + if ((s_zone[0][i].fRe != 0.0f) || (s_zone[0][i].fIm != 0.0f) || + (s_zone[0][i].fAm != 0.0f) || (s_zone[0][i].fAmSQ != 0.0f) || + (s_zone[1][i].fRe != 0.0f) || (s_zone[1][i].fIm != 0.0f) || + (s_zone[1][i].fAm != 0.0f) || (s_zone[1][i].fAmSQ != 0.0f)) { + zones_dirty = 1; + } + } + CHECK(zones_dirty == 0, "TC-PH-031 phasor_init zeroes both result zones"); + CHECK(s_use_zone == 0u, "TC-PH-032 phasor_init resets use zone to 0"); + CHECK(phasor_get(0u, &p) == -2, "TC-PH-033 phasor_init clears ready (get -> -2)"); + + /* 3) 幂等:重复 init 不改变结果 */ + (void)phasor_init(); + CHECK((s_use_zone == 0u) && (phasor_get(0u, &p) == -2), "TC-PH-034 phasor_init is idempotent"); +} + +/* ===================== TC-PH-022:L2-7 双区指针原子交换 ===================== */ + +static void tc_dualzone(void) { + const double amp = 100.0; + float f[ADC_SEM_COUNT]; + phasor_t p; + uint32_t t; + int ok; + + /* 1) 使用区逐次产出交替(0 -> 1 -> 0 -> 1) */ + phasor_init(); + for (t = 0u; t < 20u; t++) { + frame_at(f, t, amp, 0.0); + (void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT); + phasor_run(); + } + CHECK(s_use_zone == 1u, "L2-7 use zone == 1 after 1st production"); + + frame_at(f, 20u, amp, 0.0); + (void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT); + phasor_run(); + CHECK(s_use_zone == 0u, "L2-7 use zone == 0 after 2nd production"); + + frame_at(f, 21u, amp, 0.0); + (void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT); + phasor_run(); + CHECK(s_use_zone == 1u, "L2-7 use zone == 1 after 3rd production"); + + /* 2) 每拍产出后 get() 必须等于"当前环窗的独立重算"。 + * 幅值每 20 拍切换(100 <-> 10),故"读到陈旧区"会立刻不相等—— + * 连测 60 拍,覆盖两个区的来回切换,比单次比较强得多。 */ + phasor_init(); + ok = 1; + for (t = 0u; t < 60u; t++) { + float win[PHASOR_N]; + phasor_t ref, got; + double a = (((t / 20u) % 2u) == 0u) ? amp : (amp / 10.0); + + frame_at(f, t, a, 0.0); + (void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT); + phasor_run(); + + if (sample_ring_window(ADC_SEM_IA, (uint8_t)PHASOR_N, win) != 0) { + continue; /* 尚未攒满一周波:本拍按契约不产出,无需比较 */ + } + phasor_calc(win, (uint8_t)PHASOR_N, 1u, &ref); + if (phasor_get(0u, &got) != 0) { ok = 0; break; } + if ((ref.fRe != got.fRe) || (ref.fIm != got.fIm) || (ref.fAmSQ != got.fAmSQ)) { ok = 0; break; } + } + CHECK(ok, "L2-7 dual-zone: get() == fresh recompute of the live window, all 60 beats"); + CHECK(phasor_get(0u, &p) == 0, "L2-7 result still readable after the sweep"); +} + +int main(void) { + printf("=== Phasor Tests ===\n"); + + printf("-- L2-1 coefficient table --\n"); + tc_table(); + printf("-- L2-3/L2-4 fundamental amplitude/RMS/phase --\n"); + tc_fundamental(); + printf("-- DC/harmonic rejection --\n"); + tc_filtering(); + printf("-- arbitrary harmonic extraction --\n"); + tc_harmonic(); + printf("-- frequency sensitivity --\n"); + tc_freqdev(); + printf("-- L2 input sample ring --\n"); + tc_ring(); + printf("-- L2-2/L2-6/L2-7 instance layer --\n"); + tc_instances(); + printf("-- L2-7 dual-zone swap --\n"); + tc_dualzone(); + printf("-- L2 zero / init --\n"); + tc_zero(); + + printf("\n=== Phasor Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/phasor/run_unit_test.py b/test/host/phasor/run_unit_test.py new file mode 100644 index 00000000..a916aca0 --- /dev/null +++ b/test/host/phasor/run_unit_test.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""相量层(L2)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/phasor/phasor_test.c(它以 #include 纳入**生产源文件** + App/algo/phasor.c 与 App/algo/sample_ring.c),在 PC 上离线跑完整的相量算法验证: + 系数表逐项复核 / 幅值-有效值-相位 / 直流与谐波抑制 / 任意次谐波提取 / + 频率敏感性 / 采样环时序与回绕 / 实例层与双区交换,失败即非 0 退出。 + 零硬件、零烧录、毫秒级。 + + 工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness + `test/host/common/unit_test_harness.py`(本文件只做声明)。 + +为什么要主机侧单测(而不是只用板端套件): + 相量算法是纯数学变换,与硬件无关。**板端无信号源**,无法注入已知幅值/相位/频率 + 的标准波形,因而无法验证"读数 = 设定有效值"这一 L2-3 核心判据(与 L1-3 的替代口径 + 同源)。主机侧可合成任意波形并精确构造边界(恰好一周波 / 不足一周波 / 环形回绕 / + 任意次谐波 / 频偏),且不占 MCU 的 RAM。 + +本层为**纯算法**:无寄存器、无 RTOS、无文件/外设依赖,故**不需要任何桩目录**。 + +用法(仓库根目录): + python test/host/phasor/run_unit_test.py + 或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/phasor/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:docs/算法/继电保护功能复现规划.md §3.2(L2-1/2/3/4/6/7); + docs/算法/傅氏算法详解.md §2.3(系数口径); + docs/测试说明/测试开发标准流程.md §4.2(L0.5 主机侧单测)。 +创建日期:2026-10-10 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +# harness 位于 test/host/common/ +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 无桩目录:被测代码为纯算法(phasor.c / sample_ring.c 仅依赖 math.h 与 App/algo 内的头)。 +# App/algo 提供 phasor.h / sample_ring.h / adc_input.h(后者为纯枚举 + 声明,可主机编译)。 +BUILD = TestBuild( + name="Phasor Tests", + src=os.path.join(HERE, "phasor_test.c"), + include_dirs=[ + os.path.join(ROOT, "App", "algo"), + HERE, + ], + summary_marker="=== Phasor Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/prot_elm/prot_elm_test.c b/test/host/prot_elm/prot_elm_test.c new file mode 100644 index 00000000..bd755cb4 --- /dev/null +++ b/test/host/prot_elm/prot_elm_test.c @@ -0,0 +1,302 @@ +/* + * 模块名称:保护元件框架(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-PE-010 跨上下文复归请求(B5):request 不当拍生效、下一拍 eval 头部消费 + * + * 通过:全部 TC PASS(main 返回 0)。 + * 创建日期:2026-10-11 + */ +#include +#include + +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"); +} + +static void tc_request_reset(void) { + float m[PE_COUNT]; + prot_elm_state_t st; + + prot_set_init(); + prot_elm_init(); + mk_base(m); + m[PE_OC1] = 999.0f; /* OC1 默认 dly=0 → 启动同拍动作 */ + prot_elm_eval(1000u, m); + prot_elm_state(PE_OC1, &st); + CHECK(st.asserted != 0u, "TC-PE-010 pre: OC1 asserted (default dly=0)"); + + prot_elm_request_reset(); /* 跨上下文请求:不当拍生效 */ + prot_elm_state(PE_OC1, &st); + CHECK(st.asserted != 0u, "TC-PE-010 request alone does not reset"); + + /* 下一拍头部消费请求 → 复位;测量落在滞回保持区(9.6 ∈ [返回9.5, 动作10)) + * ——不复位则 picked/asserted 保持,复位后保持清零,可区分 */ + m[PE_OC1] = 9.6f; + prot_elm_eval(1100u, m); + prot_elm_state(PE_OC1, &st); + CHECK((st.picked == 0u) && (st.asserted == 0u), + "TC-PE-010 next eval consumes request -> cleared in hysteresis band"); +} + +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(); + tc_request_reset(); + + printf("\n=== Prot Elm Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/prot_elm/run_unit_test.py b/test/host/prot_elm/run_unit_test.py new file mode 100644 index 00000000..df79edff --- /dev/null +++ b/test/host/prot_elm/run_unit_test.py @@ -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)) diff --git a/test/host/prot_set/prot_set_test.c b/test/host/prot_set/prot_set_test.c new file mode 100644 index 00000000..c40907ec --- /dev/null +++ b/test/host/prot_set/prot_set_test.c @@ -0,0 +1,259 @@ +/* + * 模块名称:保护定值模型(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-PS-008 全局软压板 PS_GLOBAL_EN(B7):默认投入、set_sealed 原子写 + * 不破坏 selfcheck、脏标志三件套(peek/take/mark) + * TC-PS-009 持久化 blob 往返:serialize → init 清场 → parse 恢复 + * (位精确 + 封存自洽 + parse 不置脏) + * TC-PS-010 损坏 blob 整表拒绝(all-or-nothing):值区位翻转/坏 magic/ + * 截断/超长/坏版本/伪造自洽越界 → 全部拒绝且活跃值区一字不动 + * + * 通过:全部 TC PASS(main 返回 0)。 + * 创建日期:2026-10-11 + */ +#include +#include + +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"); + CHECK(prot_set_set_sealed(PS_ID_COUNT, 0) == -1, "TC-PS-007 set_sealed id guard"); +} + +/* TC-PS-008(B7:全局软压板 + 原子写 + 脏标志) */ +static void tc_global_plate_dirty(void) { + prot_set_init(); + CHECK(prot_set_dirty_peek() == 0u, "TC-PS-008 init not dirty"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-PS-008 global plate default IN"); + CHECK(prot_set_set(PS_GLOBAL_EN, PROT_SET_Q10(2)) == -1, "TC-PS-008 plate rejects 2"); + CHECK(prot_set_set_sealed(PS_GLOBAL_EN, 0) == 0, "TC-PS-008 plate OUT via set_sealed"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-PS-008 plate OUT readable"); + CHECK(prot_set_selfcheck() == 0, "TC-PS-008 set_sealed leaves selfcheck PASS (no unsealed window)"); + CHECK(prot_set_locked() == 0u, "TC-PS-008 set_sealed no lock"); + CHECK(prot_set_dirty_peek() == 1u, "TC-PS-008 set_sealed marks dirty"); + CHECK(prot_set_dirty_take() == 1u, "TC-PS-008 dirty take consumes"); + CHECK(prot_set_dirty_peek() == 0u, "TC-PS-008 dirty cleared after take"); + prot_set_dirty_mark(); + CHECK(prot_set_dirty_peek() == 1u, "TC-PS-008 dirty mark re-arms (write-fail retry)"); + CHECK(prot_set_set_sealed(PS_GLOBAL_EN, PROT_SET_Q10(2)) == -1, "TC-PS-008 set_sealed range check"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-PS-008 rejected set_sealed keeps value"); +} + +/* TC-PS-009(B7:blob 往返) */ +static void tc_store_roundtrip(void) { + uint8_t blob[PROT_SET_BLOB_SZ]; + uint32_t len = 0u; + int32_t cur; + + prot_set_init(); + (void)prot_set_set_sealed(PS_OC1_CUR, PROT_SET_Q10(12.5)); + (void)prot_set_set_sealed(PS_GLOBAL_EN, 0); + (void)prot_set_dirty_take(); + cur = prot_set_get(PS_OC1_CUR); + CHECK(prot_set_store_serialize(blob, sizeof(blob), &len) == 0, "TC-PS-009 serialize ok"); + CHECK(len == (uint32_t)PROT_SET_BLOB_SZ, "TC-PS-009 blob exact size"); + prot_set_init(); + CHECK(prot_set_get(PS_OC1_CUR) != cur, "TC-PS-009 init wiped to defaults"); + CHECK(prot_set_store_parse(blob, len) == 0, "TC-PS-009 parse ok"); + CHECK(prot_set_get(PS_OC1_CUR) == cur, "TC-PS-009 value round-trip bit-exact"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-PS-009 plate state round-trip"); + CHECK(prot_set_selfcheck() == 0, "TC-PS-009 parsed table sealed"); + CHECK(prot_set_dirty_peek() == 0u, "TC-PS-009 parse does not mark dirty"); +} + +/* 伪造辅助:按 blob 值区重算尾部 sum/sumn(构造"自洽但越界"的对抗样本) */ +static void s_forge_tail(uint8_t *b, uint32_t len) { + uint32_t sum = 0u; + uint32_t inv; + uint32_t i; + + for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) { + sum += s_get32(b + PROT_SET_BLOB_HDR_SZ + 4u * i); + } + inv = ~sum; + s_put32(b + len - 8u, sum); + s_put32(b + len - 4u, inv); +} + +/* TC-PS-010(B7:损坏 blob 整表拒绝,all-or-nothing) */ +static void tc_store_corrupt(void) { + uint8_t blob[PROT_SET_BLOB_SZ]; + uint32_t len = 0u; + int32_t keep; + + prot_set_init(); + (void)prot_set_set_sealed(PS_OC2_CUR, PROT_SET_Q10(6.0)); + keep = prot_set_get(PS_OC2_CUR); + CHECK(prot_set_store_serialize(blob, sizeof(blob), &len) == 0, "TC-PS-010 serialize ok"); + + blob[PROT_SET_BLOB_HDR_SZ] ^= 0x01u; /* 值区一位翻转:sum 对不符 */ + CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-010 flipped value byte rejected (sum pair)"); + CHECK(prot_set_get(PS_OC2_CUR) == keep, "TC-PS-010 reject leaves values untouched"); + + (void)prot_set_store_serialize(blob, sizeof(blob), &len); + blob[0] ^= 0xFFu; /* magic 破坏 */ + CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-010 bad magic rejected"); + + (void)prot_set_store_serialize(blob, sizeof(blob), &len); + CHECK(prot_set_store_parse(blob, len - 4u) == -1, "TC-PS-010 truncated blob rejected"); + CHECK(prot_set_store_parse(blob, len + 4u) == -1, "TC-PS-010 oversized blob rejected"); + + (void)prot_set_store_serialize(blob, sizeof(blob), &len); + blob[5] ^= 0x02u; /* version 高字节:1 -> 0x200 */ + CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-010 version mismatch rejected"); + + /* 对抗样本:越限定值 + 重算 sum 对(校验全过、范围必拦) */ + (void)prot_set_store_serialize(blob, sizeof(blob), &len); + s_put32(blob + PROT_SET_BLOB_HDR_SZ + 4u * (uint32_t)PS_OC1_CUR, + (uint32_t)(g_prot_set_meta[PS_OC1_CUR].max + 1)); + s_forge_tail(blob, len); + CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-010 forged consistent out-of-range rejected"); + CHECK(prot_set_bad_mask() == (1uL << PS_OC1_CUR), "TC-PS-010 bad_mask points at item"); + CHECK(prot_set_get(PS_OC1_CUR) == PROT_SET_Q10(10.0), "TC-PS-010 defaults still intact"); + CHECK(prot_set_selfcheck() == 0, "TC-PS-010 active table still self-consistent after all rejects"); +} + +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(); + tc_global_plate_dirty(); + tc_store_roundtrip(); + tc_store_corrupt(); + + printf("\n=== Prot Set Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/prot_set/run_unit_test.py b/test/host/prot_set/run_unit_test.py new file mode 100644 index 00000000..3778894c --- /dev/null +++ b/test/host/prot_set/run_unit_test.py @@ -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)) diff --git a/test/host/sha256/run_unit_test.py b/test/host/sha256/run_unit_test.py new file mode 100644 index 00000000..1ccb432b --- /dev/null +++ b/test/host/sha256/run_unit_test.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""SHA-256(sha256)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/sha256/sha256_test.c(它以 #include 纳入 + **生产源文件** App/util/sha256.c),在 PC 上离线跑 FIPS 180-4 标准向量、 + 流式等价、hex_eq 边界,失败即非 0 退出。零硬件、零烧录、毫秒级。 + + 工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness + `test/host/common/unit_test_harness.py`(本文件只做声明)。 + +为什么要主机侧单测(而不是只用板端): + sha256 是 FTP pwd_hash 校验(N-19)的地基——哈希算错是"正确口令被拒 / + 错误口令放行"的静默语义错误,板端只见 530/230 看不出根因;标准向量 + (空串/abc/56B 补位跨界/1MB 长度进第二分组)主机侧一次钉死。 + 本模块零平台依赖,无需任何桩。 + +用法(仓库根目录): + python test/host/sha256/run_unit_test.py + 或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/sha256/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:App/util/sha256.h;创建日期: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))) +# harness 位于 test/host/common/ +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="SHA-256 Tests", + src=os.path.join(HERE, "sha256_test.c"), + include_dirs=[ + os.path.join(ROOT, "App", "util"), + HERE, + ], + summary_marker="=== SHA-256 Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/sha256/sha256_test.c b/test/host/sha256/sha256_test.c new file mode 100644 index 00000000..160d91c0 --- /dev/null +++ b/test/host/sha256/sha256_test.c @@ -0,0 +1,192 @@ +/* + * 模块名称:SHA-256 主机侧单元测试 + * 模块功能:在 PC 上对 App/util/sha256.c 做离线验证(FIPS 180-4 标准向量 + + * 流式等价 + sha256_hex_eq 边界)。 + * + * 被测对象:真实源文件 ../../../App/util/sha256.c(以 #include 原样纳入, + * 保证测的是生产代码而非副本;本模块零平台依赖,无需任何桩)。 + * + * 为什么要主机侧单测(而不是只靠台架): + * sha256 是 FTP pwd_hash 校验(N-19)的地基——哈希算错的后果是 + * "正确口令被拒 / 错误口令放行"这类静默语义错误,板端只看到 530/230, + * 看不出哪一步坏;标准向量在主机侧毫秒级钉死。 + * + * 覆盖点: + * TC-SH-001 FIPS 180-4 向量:空串 + * TC-SH-002 FIPS 180-4 向量:"abc" + * TC-SH-003 FIPS 180-4 向量:56 字节(补位跨分组边界) + * TC-SH-004 FIPS 180-4 向量:1,000,000 × 'a'(流式,长度字段进第二分组) + * TC-SH-005 流式任意切分与一次性计算等价(逐字节喂入) + * TC-SH-006 hex_eq:小写匹配 / 大写匹配 + * TC-SH-007 hex_eq:内容不等 / 非法字符 / 长度 63/65 / NULL → 全 0 + * TC-SH-008 默认口令哈希钉值:SHA256("adminadmin") = FTP_CFG_DEF_HASH 同源值 + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-11 + */ + +#include +#include + +/* 生产源文件原样纳入(零平台依赖,直接编) */ +#include "sha256.c" + +/* ===================== 断言框架 ===================== */ + +static int s_total; +static int s_passed; +static int s_failed; + +#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) + +/* 摘要 → hex 小写串(仅测试用,允许栈上 sprintf) */ +static void digest_to_hex(const uint8_t d[SHA256_DIGEST_SIZE], char out[65]) +{ + static const char hexd[] = "0123456789abcdef"; + + for (int i = 0; i < 32; i++) { + out[2 * i] = hexd[(d[i] >> 4) & 0xF]; + out[2 * i + 1] = hexd[d[i] & 0xF]; + } + out[64] = '\0'; +} + +static int hex_eq_vector(const uint8_t d[SHA256_DIGEST_SIZE], const char *want) +{ + char got[65]; + + digest_to_hex(d, got); + return strcmp(got, want) == 0; +} + +/* ===================== TC-SH-001..004:标准向量 ===================== */ + +static void tc_vectors(void) +{ + uint8_t d[SHA256_DIGEST_SIZE]; + + sha256_calc("", 0u, d); + CHECK(hex_eq_vector(d, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), + "TC-SH-001 空串向量"); + + sha256_calc("abc", 3u, d); + CHECK(hex_eq_vector(d, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"), + "TC-SH-002 \"abc\" 向量"); + + sha256_calc("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 56u, d); + CHECK(hex_eq_vector(d, "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"), + "TC-SH-003 56 字节向量(补位跨分组)"); + + { + sha256_ctx_t ctx; + + sha256_init(&ctx); + for (int i = 0; i < 1000000; i++) { + sha256_update(&ctx, "a", 1u); + } + sha256_final(&ctx, d); + } + CHECK(hex_eq_vector(d, "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"), + "TC-SH-004 1e6 × 'a' 向量(流式)"); +} + +/* ===================== TC-SH-005:流式等价 ===================== */ + +static void tc_stream_equiv(void) +{ + static const char msg[] = "the quick brown fox jumps over the lazy dog"; + uint8_t d1[SHA256_DIGEST_SIZE]; + uint8_t d2[SHA256_DIGEST_SIZE]; + sha256_ctx_t ctx; + + sha256_calc(msg, strlen(msg), d1); + sha256_init(&ctx); + for (size_t i = 0; i < strlen(msg); i++) { + sha256_update(&ctx, &msg[i], 1u); + } + sha256_final(&ctx, d2); + CHECK(memcmp(d1, d2, sizeof(d1)) == 0, "TC-SH-005 逐字节喂入与一次性等价"); +} + +/* ===================== TC-SH-006..007:hex_eq ===================== */ + +static void tc_hex_eq(void) +{ + uint8_t d[SHA256_DIGEST_SIZE]; + char lower[65]; + char upper[65]; + char shortv[65]; + char longv[66]; + + sha256_calc("adminadmin", 10u, d); + digest_to_hex(d, lower); + for (int i = 0; i < 64; i++) { + upper[i] = (char)((lower[i] >= 'a') ? (lower[i] - 32) : lower[i]); + } + upper[64] = '\0'; + + CHECK(sha256_hex_eq(d, lower) == 1, "TC-SH-006a 小写 hex 匹配"); + CHECK(sha256_hex_eq(d, upper) == 1, "TC-SH-006b 大写 hex 匹配(大小写不敏感)"); + + lower[63] = (char)((lower[63] == '0') ? '1' : '0'); + CHECK(sha256_hex_eq(d, lower) == 0, "TC-SH-007a 内容不等 → 0"); + lower[63] = (char)((lower[63] == '0') ? '1' : '0'); /* 还原 */ + + { + char bad[65]; + + memcpy(bad, lower, 64); + bad[64] = '\0'; + bad[10] = 'g'; + CHECK(sha256_hex_eq(d, bad) == 0, "TC-SH-007b 非法 hex 字符 → 0"); + } + + memcpy(shortv, lower, 63); + shortv[63] = '\0'; + CHECK(sha256_hex_eq(d, shortv) == 0, "TC-SH-007c 长度 63 → 0"); + + memcpy(longv, lower, 64); + longv[64] = 'a'; + longv[65] = '\0'; + CHECK(sha256_hex_eq(d, longv) == 0, "TC-SH-007d 长度 65 → 0"); + + CHECK(sha256_hex_eq(d, NULL) == 0, "TC-SH-007e NULL → 0"); +} + +/* ===================== TC-SH-008:默认口令哈希钉值 ===================== */ + +static void tc_default_hash(void) +{ + uint8_t d[SHA256_DIGEST_SIZE]; + + /* 与 ftp_cfg.h FTP_CFG_DEF_HASH 同源钉值(两处若漂移本测试立刻红) */ + sha256_calc("adminadmin", 10u, d); + CHECK(sha256_hex_eq(d, "d82494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892") == 1, + "TC-SH-008 SHA256(\"adminadmin\") = 出厂默认 pwd_hash"); +} + +/* ===================== main ===================== */ + +int main(void) +{ + printf("=== SHA-256 Tests ===\n"); + + printf("-- FIPS 180-4 vectors --\n"); + tc_vectors(); + printf("-- streaming equivalence --\n"); + tc_stream_equiv(); + printf("-- hex_eq --\n"); + tc_hex_eq(); + printf("-- default password hash --\n"); + tc_default_hash(); + + printf("\n=== SHA-256 Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/time_sync/run_unit_test.py b/test/host/time_sync/run_unit_test.py new file mode 100644 index 00000000..664871ce --- /dev/null +++ b/test/host/time_sync/run_unit_test.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""时间同步(time_sync)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/time_sync/time_sync_test.c(它以 #include 纳入 + **生产源文件** App/sync/time_sync.c),在 PC 上离线跑: + 分片延时(心跳节奏)/ 监听建立与退避 / 时间包重组状态机(分段 + 30s 预算 + + tick 回绕)/ 任务主循环(就绪屏障、accept 轮询、代次变化重监听)。 + 失败即非 0 退出。零硬件、零烧录、毫秒级。 + + 工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness + `test/host/common/unit_test_harness.py`(本文件只做声明)。 + +为什么要主机侧单测(而不是只用板端套件): + 本模块是登记簿 N-17 的当事方——"监听重建期间心跳缺口 800ms~1s,距 IWDG 最坏 + 超时 1701ms 余量仅 ~0.7s"。缺口成因全是本模块的**纯逻辑**(分片延时的喂狗 + 节奏、500/2000ms 监听退避、200ms accept 退避、50ms 收尾、屏障等待态喂心跳)。 + 真机上只能"跑起来看复位不复位",主机侧可**逐片断言**。时间包重组状态机 + (分段送达 / 预算耗尽 / EOF / 真错误 / tick 回绕)同理。 + +用法(仓库根目录): + python test/host/time_sync/run_unit_test.py + 或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/time_sync/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:App/sync/time_sync.c 文件头注释 + docs/任务规划/登记簿.md N-17; +创建日期:2026-10-10 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +# harness 位于 test/host/common/ +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 顺序敏感:stub 目录必须**先于**生产目录,才能覆盖 FreeRTOS.h / task.h / +# net_socket.h / sys_events.h / sys_clock.h / dbg_log.h(真实头会拖入整套 +# FreeRTOS / CMSIS-RTOS / HAL)。task_watchdog.h 用**真实头**(App/util,只依赖 +# stdint.h),time_sync.h 用真实头(接口契约原汁)。 +BUILD = TestBuild( + name="Time Sync Tests", + src=os.path.join(HERE, "time_sync_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "App", "util"), + os.path.join(ROOT, "App", "sync"), + HERE, + ], + summary_marker="=== Time Sync Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/time_sync/stub/FreeRTOS.h b/test/host/time_sync/stub/FreeRTOS.h new file mode 100644 index 00000000..0bfaf684 --- /dev/null +++ b/test/host/time_sync/stub/FreeRTOS.h @@ -0,0 +1,34 @@ +/* + * 主机侧单测桩:FreeRTOS.h + * 模块功能:为 App/sync/time_sync.c 提供主机可编译的最小 FreeRTOS 时基类型与宏。 + * + * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 + * 真实头在 Middlewares/Third_Party/FreeRTOS/Source/include/,会拖入 portmacro / + * projdefs / portable 层,主机侧不引入。 + * + * 保真点:TickType_t = uint32_t;configTICK_RATE_HZ = 1000(Inc/FreeRTOSConfig.h) + * → pdMS_TO_TICKS(ms) == ms。被断言"片长 = 50ms"的用例依赖此等式。 + * + * 创建日期:2026-10-10 + */ + +#ifndef FREERTOS_H +#define FREERTOS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef uint32_t TickType_t; + +/* 与 Inc/FreeRTOSConfig.h 一致:1000 Hz → 1 tick = 1 ms */ +#define configTICK_RATE_HZ 1000U +#define pdMS_TO_TICKS(ms) ((TickType_t)((uint32_t)(ms) * (1000U / configTICK_RATE_HZ))) + +#ifdef __cplusplus +} +#endif + +#endif /* FREERTOS_H */ diff --git a/test/host/time_sync/stub/cmsis_os2.h b/test/host/time_sync/stub/cmsis_os2.h new file mode 100644 index 00000000..c49d0f59 --- /dev/null +++ b/test/host/time_sync/stub/cmsis_os2.h @@ -0,0 +1,59 @@ +/* + * 主机侧单测桩:cmsis_os2.h + * 模块功能:为 App/sync/time_sync.c(经 sys_events.h)提供主机可编译的 CMSIS-RTOS v2 + * 事件标志接口与类型。 + * + * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 + * 真实头在 Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/,会拖入整套 + * FreeRTOS + CMSIS 内核对象,主机侧不引入。 + * + * 保真点(对齐 cmsis_os2.h): + * - osEventFlagsId_t = void*(:195)、osMutexId_t = void*(:198); + * - osStatus_t 枚举含 osOK = 0; + * - 标志位 osFlagsWaitAny=0 / osFlagsNoClear=0x2 / osFlagsError=0x80000000 + * / osFlagsErrorTimeout=0xFFFFFFFE(:153~164); + * - uint32_t osEventFlagsWait(id, flags, options, timeout)(:539)。 + * osEventFlagsWait 由测试单元给强符号桩(可注入"未就绪/超时"并驱动屏障循环)。 + * + * 创建日期:2026-10-10 + */ + +#ifndef CMSIS_OS2_H +#define CMSIS_OS2_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 不透明句柄 */ +typedef void *osEventFlagsId_t; +typedef void *osMutexId_t; + +/* 状态码(只保留被测代码用到的子集) */ +typedef enum { + osOK = 0, + osError = -1, + osErrorTimeout = -2, + osErrorResource = -3, + osErrorParameter = -4 +} osStatus_t; + +/* 事件标志选项(cmsis_os2.h:153~164) */ +#define osFlagsWaitAny 0x00000000U +#define osFlagsWaitAll 0x00000001U +#define osFlagsNoClear 0x00000002U +#define osFlagsError 0x80000000U +#define osFlagsErrorUnknown 0xFFFFFFFFU +#define osFlagsErrorTimeout 0xFFFFFFFEU +#define osFlagsErrorResource 0xFFFFFFFDU +#define osFlagsErrorParameter 0xFFFFFFFCU + +uint32_t osEventFlagsWait(osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout); + +#ifdef __cplusplus +} +#endif + +#endif /* CMSIS_OS2_H */ diff --git a/test/host/time_sync/stub/dbg_log.h b/test/host/time_sync/stub/dbg_log.h new file mode 100644 index 00000000..e24d84bd --- /dev/null +++ b/test/host/time_sync/stub/dbg_log.h @@ -0,0 +1,31 @@ +/* + * 主机侧单测桩:dbg_log.h + * 模块功能:为 App/sync/time_sync.c 提供主机可编译的日志宏(映射到计数)。 + * + * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 + * + * 注意:time_sync.c 在 `#include "dbg_log.h"` 之前自行 `#define` 了 + * DBG_LINE_BUF_MAX 与 DBG_TAG(真实头会用到它们),故本桩**不得**再定义这两个宏, + * 否则触发 C4005 宏重定义告警(/W4)。 + * + * DBG_ERROR / DBG_INFO 计入计数器,供单测断言: + * - "net_socket/bind/listen FAIL"、"open listener FAIL, retry later" → ERROR + * - "no TIME packet in 30s, drop conn"、"wall clock updated"、"network re-init" → INFO + * + * 创建日期:2026-10-10 + */ + +#ifndef DBG_LOG_H +#define DBG_LOG_H + +#include + +/* 计数(在 time_sync_test.c 实现) */ +extern int g_tstest_err_count; +extern int g_tstest_info_count; + +#define DBG_INFO(fmt, ...) do { g_tstest_info_count++; } while (0) +#define DBG_DEBUG(fmt, ...) do { } while (0) +#define DBG_ERROR(fmt, ...) do { g_tstest_err_count++; } while (0) + +#endif /* DBG_LOG_H */ diff --git a/test/host/time_sync/stub/net_socket.h b/test/host/time_sync/stub/net_socket.h new file mode 100644 index 00000000..92b1df16 --- /dev/null +++ b/test/host/time_sync/stub/net_socket.h @@ -0,0 +1,71 @@ +/* + * 主机侧单测桩:net_socket.h(含内联 net_types.h 子集) + * 模块功能:为 App/sync/time_sync.c 提供主机可编译的网络 Socket API 声明与类型。 + * + * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 + * 真实头(Drivers/BSP/NET/net_socket.h)包含 net_types.h → net_config.h,并经 + * FreeRTOS.h / task.h 拖入整套 FreeRTOS;本桩改为包含同名桩头。 + * + * 保真点: + * - 地址族/类型/特殊地址常量与 Drivers/BSP/NET/net_types.h 逐字一致; + * - struct net_sockaddr / net_in_addr / net_sockaddr_in 字段与真实头一致 + * (被测代码写 sin_family / sin_port / sin_addr.s_addr,桩按同布局读取); + * - 7 个被测代码调用的接口签名与真实头一致(net_socket / net_bind / net_listen / + * net_accept / net_close / net_recv_to / net_get_generation / net_htons)。 + * 全部由测试单元给强符号桩(脚本化返回值 + 调用记录)。 + * + * 创建日期:2026-10-10 + */ + +#ifndef NET_SOCKET_H +#define NET_SOCKET_H + +#include + +#include "FreeRTOS.h" +#include "task.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ===== 以下取自 Drivers/BSP/NET/net_types.h(逐字) ===== */ + +#define NET_AF_INET 2 /* IPv4 */ +#define NET_SOCK_STREAM 1 /* TCP */ +#define NET_SOCK_DGRAM 2 /* UDP */ +#define NET_INADDR_ANY 0 /* 任意地址 */ +#define NET_INADDR_NONE 0xFFFFFFFF /* 无效地址 */ + +struct net_sockaddr { + uint16_t sa_family; /* 地址族 */ + char sa_data[14]; /* 地址数据 */ +}; + +struct net_in_addr { + uint32_t s_addr; /* IP 地址(网络字节序) */ +}; + +struct net_sockaddr_in { + uint16_t sin_family; /* 地址族,必须为 AF_INET */ + uint16_t sin_port; /* 端口号(网络字节序) */ + struct net_in_addr sin_addr; /* IP 地址(网络字节序) */ + char sin_zero[8]; /* 填充字节 */ +}; + +/* ===== 被测代码用到的 Socket API(签名与真实头一致) ===== */ + +int net_socket(int domain, int type); +int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen); +int net_listen(int sockfd); +int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen); +int net_close(int sockfd); +int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms); +uint32_t net_get_generation(void); +uint16_t net_htons(uint16_t hostshort); + +#ifdef __cplusplus +} +#endif + +#endif /* NET_SOCKET_H */ diff --git a/test/host/time_sync/stub/sys_clock.h b/test/host/time_sync/stub/sys_clock.h new file mode 100644 index 00000000..81c19706 --- /dev/null +++ b/test/host/time_sync/stub/sys_clock.h @@ -0,0 +1,29 @@ +/* + * 主机侧单测桩:sys_clock.h + * 模块功能:为 App/sync/time_sync.c 提供主机可编译的墙钟设置接口声明。 + * + * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 + * 真实头(App/util/sys_clock.h)含日历/闰年等无关声明;本桩只保留被测代码 + * 真正调用的那一个(签名与真实头逐字一致:int sys_clock_set_unix(uint32_t))。 + * + * 由测试单元给强符号桩,记录"是否被调用"与"最后一次的 epoch"。 + * + * 创建日期:2026-10-10 + */ + +#ifndef SYS_CLOCK_H +#define SYS_CLOCK_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int sys_clock_set_unix(uint32_t unix_secs); + +#ifdef __cplusplus +} +#endif + +#endif /* SYS_CLOCK_H */ diff --git a/test/host/time_sync/stub/sys_events.h b/test/host/time_sync/stub/sys_events.h new file mode 100644 index 00000000..3840d899 --- /dev/null +++ b/test/host/time_sync/stub/sys_events.h @@ -0,0 +1,39 @@ +/* + * 主机侧单测桩:sys_events.h + * 模块功能:为 App/sync/time_sync.c 提供主机可编译的系统就绪事件组句柄与事件位。 + * + * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 + * 真实头(App/app/sys_events.h)包含 cmsis_os2.h;本桩改为包含同名桩, + * 避免拖入整套 CMSIS-RTOS/FreeRTOS。 + * + * 事件位取值与真实头逐位一致(本模块只用到 SYS_EVT_NET_READY)。 + * + * 创建日期:2026-10-10 + */ + +#ifndef SYS_EVENTS_H +#define SYS_EVENTS_H + +#include "cmsis_os2.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* 系统就绪事件组句柄(真实工程由 MX_FREERTOS_Init 创建;主机侧由测试单元定义) */ +extern osEventFlagsId_t sys_evtHandle; + +/* 各子系统"就绪"事件位(与 App/app/sys_events.h 一致) */ +#define SYS_EVT_NET_READY (1u << 0) +#define SYS_EVT_STORAGE_READY (1u << 1) +#define SYS_EVT_FTP_READY (1u << 2) +#define SYS_EVT_TIMESYNC_READY (1u << 3) +#define SYS_EVT_NET_RECONFIG (1u << 4) +#define SYS_EVT_CFG_SAVE (1u << 5) +#define SYS_EVT_CONSOLE_TX (1u << 6) + +#ifdef __cplusplus +} +#endif + +#endif /* SYS_EVENTS_H */ diff --git a/test/host/time_sync/stub/task.h b/test/host/time_sync/stub/task.h new file mode 100644 index 00000000..e6b6cbc7 --- /dev/null +++ b/test/host/time_sync/stub/task.h @@ -0,0 +1,29 @@ +/* + * 主机侧单测桩:task.h + * 模块功能:为 App/sync/time_sync.c 提供主机可编译的 FreeRTOS 任务时基接口声明。 + * + * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 + * + * 两个接口由测试单元给强符号桩:vTaskDelay 记录每片睡眠时长(断言"≤50ms 分片"), + * xTaskGetTickCount 由测试单元提供可控时钟(驱动 30s 预算耗尽与 32 位回绕用例)。 + * + * 创建日期:2026-10-10 + */ + +#ifndef TASK_H +#define TASK_H + +#include "FreeRTOS.h" + +#ifdef __cplusplus +extern "C" { +#endif + +void vTaskDelay(TickType_t xTicksToDelay); +TickType_t xTaskGetTickCount(void); + +#ifdef __cplusplus +} +#endif + +#endif /* TASK_H */ diff --git a/test/host/time_sync/time_sync_test.c b/test/host/time_sync/time_sync_test.c new file mode 100644 index 00000000..734c4c72 --- /dev/null +++ b/test/host/time_sync/time_sync_test.c @@ -0,0 +1,717 @@ +/* + * 模块名称:Time Sync(PC 本地时间推送接收)主机侧单元测试 + * 模块功能:在 PC 上对 App/sync/time_sync.c 做离线验证(4 个函数全覆盖: + * time_sync_delay_fed / time_sync_open_listener / time_sync_handle_conn / + * time_sync_task)。 + * + * 被测对象:真实源文件 ../../../App/sync/time_sync.c(以 #include 原样纳入, + * 保证测的是生产代码而非副本)。平台依赖由本文件提供的**可注入桩**实现: + * - FreeRTOS/task.h -> vTaskDelay / xTaskGetTickCount(可控时钟) + * - net_socket.h -> socket/bind/listen/accept/close/recv_to/ + * get_generation/htons(全部脚本化) + * - sys_events.h -> osEventFlagsWait(可注入"未就绪") + * - sys_clock.h -> sys_clock_set_unix(记录 epoch) + * - task_watchdog.h -> task_wd_beat / task_wd_bind(记录心跳节奏) + * - dbg_log.h -> DBG_ERROR/DBG_INFO 计数 + * + * 为什么要主机侧单测(而不是只用板端套件): + * 1. **本模块是 N-17 的当事方**:登记簿 N-17 记录"time_sync 监听重建期间心跳缺口 + * 800ms~1s,距 IWDG 最坏超时 1701ms 余量仅 ~0.7s"。缺口的成因全部是本模块的 + * **纯逻辑**——分片延时(delay_fed)的心跳节奏、监听重试退避(500/2000ms)、 + * accept 轮询退避(200ms)、收尾延时(50ms)、以及"屏障等待态也要喂心跳"。 + * 这些在真机上只能靠"跑起来看复位不复位"间接观察,主机侧可**逐片断言**。 + * 2. **时间包重组状态机**(handle_conn)是纯逻辑:累积到 8 字节才解析、30s 总预算 + * 随 tick 推进耗尽、TCP 分段送达、EOF/真错误/超时三种返回码分支——板端套件 + * 无法精确构造"分段 + 预算耗尽 + tick 回绕"的组合。 + * 3. 不占 MCU 的 RAM,毫秒级完成。 + * + * 覆盖点(TC-TS-xxx): + * —— time_sync_delay_fed(可喂心跳的分片延时)—— + * TC-TS-001 0ms 不睡眠不喂狗 + * TC-TS-002 恰好一片(50ms) + * TC-TS-003 非整片(120ms)切成 50/50/20,每片喂心跳 + * TC-TS-004 长延时(2000ms)= 40 片 × 50ms,每片喂心跳 + * TC-TS-005 不足一片(49ms)单片处理 + * TC-TS-006 总预算 30s = 600 片(片长上限恒 50ms) + * —— time_sync_open_listener(监听建立与重试)—— + * TC-TS-010 首次成功:socket/bind/listen 各一次,端口经 net_htons,无退避 + * TC-TS-011 bind 失败 -> 关 socket + 退避 2000ms + 留痕,随后重试成功 + * TC-TS-012 listen 失败 -> 关 socket + 退避 2000ms,随后重试成功 + * TC-TS-013 socket 连续失败 -> 5 次后返回 -1(5 条 ERROR,5×500ms 退避) + * TC-TS-014 第 3 次重试成功并返回新描述符 + * TC-TS-015 每次重试前 + 每片延时各喂一次心跳,且心跳位恒为 TIMESYNC + * —— time_sync_handle_conn(时间包重组状态机)—— + * TC-TS-020 8 字节一次收齐 -> 大端解析 epoch + 更新墙钟 + 关连接 + * TC-TS-021 分段送达(4+4)-> 累积后解析;第二次只请求剩余 12 字节 + * TC-TS-022 一次到达 16 字节 -> 只取前 8 字节 + * TC-TS-023 前缀非 "TIME" -> 不更新墙钟,但仍关连接 + * TC-TS-024 对端关闭(n==0)-> 立即退出、不解析 + * TC-TS-025 真错误(n==-1)-> 立即退出、不解析 + * TC-TS-026 只收到 4 字节后 EOF -> 不足 8 字节不解析 + * TC-TS-027 30s 预算耗尽 -> 留痕 "no TIME packet in 30s, drop conn" + * TC-TS-028 剩余预算不足一片 -> 末片按剩余(20ms)切分 + * TC-TS-029 32 位 tick 回绕 -> 预算计算仍正确 + * —— time_sync_task(任务主循环)—— + * TC-TS-040 sys_evtHandle==NULL:跳过就绪屏障;accept 轮询->成功->处理->收尾 + * TC-TS-041 就绪屏障:100ms 分片等待 NET_READY(osFlagsNoClear 不消费就绪位) + * TC-TS-042 屏障未就绪期间持续喂心跳(2026-10-02 台架根因的回归防线) + * TC-TS-043 网络代次变化 -> 关闭旧监听并重监听 + * TC-TS-044 监听建立失败 -> 5 次重试后放弃并退避 2000ms + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-10 + */ + +#include +#include +#include +#include + +/* ===================== 平台桩(先纳入,覆盖真实头) ===================== */ +#include "FreeRTOS.h" +#include "task.h" +#include "net_socket.h" +#include "sys_clock.h" +#include "sys_events.h" +#include "task_watchdog.h" /* 真实头(App/util):task_wd_id_t / TASK_WD_TIMESYNC */ + +/* ===================== FreeRTOS 桩 ===================== */ +static TickType_t s_tick; +#define DELAY_LOG_MAX 800 +static uint32_t s_delays[DELAY_LOG_MAX]; +static int s_delay_calls; + +void vTaskDelay(TickType_t xTicksToDelay) { + if (s_delay_calls < DELAY_LOG_MAX) { + s_delays[s_delay_calls] = (uint32_t)xTicksToDelay; + } + s_delay_calls++; +} + +TickType_t xTaskGetTickCount(void) { + return s_tick; +} + +/* ===================== CMSIS-RTOS 事件标志桩 ===================== */ +osEventFlagsId_t sys_evtHandle = NULL; + +static uint32_t s_evt_seq[8]; +static int s_evt_seq_n; +static uint32_t s_evt_default = 0xFFFFFFFFu; /* 未脚本化时的返回值(默认:就绪) */ +static int s_evt_calls; +static int s_evt_longjmp_at = -1; /* >0 时第 N 次调用触发 longjmp */ +static uint32_t s_evt_last_flags; +static uint32_t s_evt_last_options; +static uint32_t s_evt_last_timeout; + +static jmp_buf s_jmp; + +uint32_t osEventFlagsWait(osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout) { + int i = s_evt_calls; + (void)ef_id; + + s_evt_last_flags = flags; + s_evt_last_options = options; + s_evt_last_timeout = timeout; + s_evt_calls++; + + if (s_evt_longjmp_at > 0 && (i + 1) >= s_evt_longjmp_at) { + longjmp(s_jmp, 2); /* 跳出 time_sync_task 的屏障循环(用例收尾) */ + } + if (i < s_evt_seq_n) { + return s_evt_seq[i]; + } + return s_evt_default; +} + +/* ===================== 看门狗桩 ===================== */ +static int s_beat_calls; +static int s_beat_id_bad; +/* ⚠ 命名必须与 net_socket 桩的 net_bind 计数区分开:C 的"暂定定义"规则下, + * 同一编译单元里两个 `static int x;` 会被**静默合并为同一个对象**(不报错), + * 于是 task_wd_bind 与 net_bind 会共用计数器,断言全部失真(2026-10-10 踩过)。 */ +static int s_wd_bind_calls; +static int s_wd_bind_id_bad; + +void task_wd_beat(task_wd_id_t id) { + s_beat_calls++; + if (id != TASK_WD_TIMESYNC) { + s_beat_id_bad++; + } +} + +void task_wd_bind(task_wd_id_t id) { + s_wd_bind_calls++; + if (id != TASK_WD_TIMESYNC) { + s_wd_bind_id_bad++; + } +} + +/* ===================== sys_clock 桩 ===================== */ +static int s_set_unix_calls; +static uint32_t s_set_unix_last; + +int sys_clock_set_unix(uint32_t unix_secs) { + s_set_unix_calls++; + s_set_unix_last = unix_secs; + return 0; +} + +/* ===================== net_socket 桩 ===================== */ +static uint16_t s_htons_last; + +static int s_socket_calls; +static int s_socket_seq[8]; +static int s_socket_seq_n; +static int s_socket_default = 5; + +static int s_bind_calls; +static int s_bind_seq[8]; +static int s_bind_seq_n; +static uint16_t s_bind_port; +static uint16_t s_bind_family; + +static int s_listen_calls; +static int s_listen_seq[8]; +static int s_listen_seq_n; + +static int s_accept_calls; +static int s_accept_seq[16]; +static int s_accept_seq_n; + +static int s_close_calls; +static int s_close_socks[32]; + +static uint32_t s_gen_seq[8]; +static int s_gen_seq_n; +static uint32_t s_gen_default = 1u; +static int s_gen_calls; +static int s_gen_longjmp_at = -1; /* >0 时第 N 次调用触发 longjmp */ + +uint16_t net_htons(uint16_t hostshort) { + s_htons_last = hostshort; + return (uint16_t)(((hostshort & 0xFFu) << 8) | (hostshort >> 8)); +} + +int net_socket(int domain, int type) { + int i = s_socket_calls++; + (void)domain; + (void)type; + return (i < s_socket_seq_n) ? s_socket_seq[i] : s_socket_default; +} + +int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) { + const struct net_sockaddr_in *a = (const struct net_sockaddr_in *)addr; + int i = s_bind_calls++; + + (void)sockfd; + (void)addrlen; + s_bind_family = a->sin_family; + s_bind_port = a->sin_port; + return (i < s_bind_seq_n) ? s_bind_seq[i] : 0; +} + +int net_listen(int sockfd) { + int i = s_listen_calls++; + (void)sockfd; + return (i < s_listen_seq_n) ? s_listen_seq[i] : 0; +} + +int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) { + int i = s_accept_calls++; + (void)sockfd; + (void)addr; + (void)addrlen; + return (i < s_accept_seq_n) ? s_accept_seq[i] : -1; +} + +int net_close(int sockfd) { + if (s_close_calls < 32) { + s_close_socks[s_close_calls] = sockfd; + } + s_close_calls++; + return 0; +} + +uint32_t net_get_generation(void) { + int i = s_gen_calls++; + + if (s_gen_longjmp_at > 0 && (i + 1) >= s_gen_longjmp_at) { + longjmp(s_jmp, 1); /* 跳出 time_sync_task 的主循环(用例收尾) */ + } + return (i < s_gen_seq_n) ? s_gen_seq[i] : s_gen_default; +} + +/* ---- net_recv_to:脚本化返回码 + 可推进 tick ---- */ +typedef struct { + int n; /* 返回码:>0 字节数 / 0 EOF / -1 错误 / -2 超时无数据 */ + uint8_t data[16]; /* 要"送达"的字节 */ + int len; /* data 有效长度 */ + uint32_t tick_adv; /* 返回前把假时钟推进多少 ms */ +} recv_step_t; + +static recv_step_t s_recv_seq[16]; +static int s_recv_seq_n; +static int s_recv_idx; +static int s_recv_calls; +static int s_recv_default = 0; /* 脚本用尽后的返回码(0=EOF,保证循环终止) */ +static uint32_t s_recv_to_log[64]; +static int s_recv_len_log[64]; + +int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { + int i = s_recv_calls; + + (void)sockfd; + if (i < 64) { + s_recv_to_log[i] = timeout_ms; + s_recv_len_log[i] = len; + } + s_recv_calls++; + + if (s_recv_idx >= s_recv_seq_n) { + return s_recv_default; + } + + { + const recv_step_t *st = &s_recv_seq[s_recv_idx]; + int copy; + s_recv_idx++; + s_tick += st->tick_adv; + if (st->n <= 0) { + return st->n; + } + copy = (st->n <= len) ? st->n : len; + if (copy > st->len) { + copy = st->len; + } + if ((copy > 0) && (buf != NULL)) { + memcpy(buf, st->data, (size_t)copy); + } + return st->n; + } +} + +/* ===================== 断言 ===================== */ +static int s_total = 0, s_passed = 0, s_failed = 0; +int g_tstest_err_count = 0; +int g_tstest_info_count = 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 "time_sync.c" + +/* ===================== 工具 ===================== */ + +/* + * 函数功能:把全部桩复位到"健康"初值 + * 入口参数:无 + * 返 回 值:无 + * 限定条件:无 + * 函数说明:默认 = 网络全成功、gen 恒为 1、recv 脚本为空(脚本用尽返回 EOF)、 + * 事件标志恒就绪。各用例按需覆盖。 + */ +static void fakes_reset(void) { + s_tick = 0; + s_delay_calls = 0; + + sys_evtHandle = NULL; + s_evt_seq_n = 0; + s_evt_default = SYS_EVT_NET_READY; /* 健康默认:就绪 */ + s_evt_calls = 0; + s_evt_longjmp_at = -1; + s_evt_last_flags = 0; + s_evt_last_options = 0; + s_evt_last_timeout = 0; + + s_beat_calls = 0; + s_beat_id_bad = 0; + s_wd_bind_calls = 0; + s_wd_bind_id_bad = 0; + + s_set_unix_calls = 0; + s_set_unix_last = 0; + + s_htons_last = 0; + s_socket_calls = 0; + s_socket_seq_n = 0; + s_socket_default = 5; + s_bind_calls = 0; + s_bind_seq_n = 0; + s_bind_port = 0; + s_bind_family = 0; + s_listen_calls = 0; + s_listen_seq_n = 0; + s_accept_calls = 0; + s_accept_seq_n = 0; + s_close_calls = 0; + s_gen_seq_n = 0; + s_gen_default = 1u; + s_gen_calls = 0; + s_gen_longjmp_at = -1; + + s_recv_seq_n = 0; + s_recv_idx = 0; + s_recv_calls = 0; + s_recv_default = 0; + + g_tstest_err_count = 0; + g_tstest_info_count = 0; +} + +/* 装配一个 recv 脚本步 */ +static void recv_set(int idx, int n, const uint8_t *data, int len, uint32_t tick_adv) { + memset(&s_recv_seq[idx], 0, sizeof(s_recv_seq[idx])); + s_recv_seq[idx].n = n; + s_recv_seq[idx].len = len; + s_recv_seq[idx].tick_adv = tick_adv; + if ((data != NULL) && (len > 0)) { + int copy = (len > 16) ? 16 : len; + memcpy(s_recv_seq[idx].data, data, (size_t)copy); + } +} + +/* 装配一个 "TIME" + uint32 大端 时间包 */ +static void make_time_packet(uint8_t *out, uint32_t epoch) { + out[0] = (uint8_t)'T'; + out[1] = (uint8_t)'I'; + out[2] = (uint8_t)'M'; + out[3] = (uint8_t)'E'; + out[4] = (uint8_t)(epoch >> 24); + out[5] = (uint8_t)(epoch >> 16); + out[6] = (uint8_t)(epoch >> 8); + out[7] = (uint8_t)(epoch); +} + +/* 检查 s_delays[0..n) 是否全部等于 v */ +static int delays_all(uint32_t v, int n) { + int k; + for (k = 0; k < n; k++) { + if (s_delays[k] != v) { + return 0; + } + } + return 1; +} + +/* ===================== TC-TS-001..006:delay_fed ===================== */ + +static void tc_delay_fed(void) { + fakes_reset(); + time_sync_delay_fed(0U); + CHECK((s_beat_calls == 0) && (s_delay_calls == 0), "TC-TS-001 delay_fed(0) 不睡眠不喂狗"); + + fakes_reset(); + time_sync_delay_fed(50U); + CHECK((s_beat_calls == 1) && (s_delay_calls == 1) && (s_delays[0] == 50U), + "TC-TS-002 delay_fed(50) = 1 片 50ms + 1 次心跳"); + + fakes_reset(); + time_sync_delay_fed(120U); + CHECK((s_beat_calls == 3) && (s_delay_calls == 3), "TC-TS-003 delay_fed(120) 切成 3 片"); + CHECK((s_delays[0] == 50U) && (s_delays[1] == 50U) && (s_delays[2] == 20U), + "TC-TS-003b 片长 50/50/20(末片为余数)"); + + fakes_reset(); + time_sync_delay_fed(2000U); + CHECK((s_delay_calls == 40) && (s_beat_calls == 40) && delays_all(50U, 40), + "TC-TS-004 delay_fed(2000) = 40 片 × 50ms,每片喂一次心跳"); + + fakes_reset(); + time_sync_delay_fed(49U); + CHECK((s_beat_calls == 1) && (s_delays[0] == 49U), "TC-TS-005 不足一片(49ms)单片处理"); + + fakes_reset(); + time_sync_delay_fed(30000U); + CHECK(s_delay_calls == 600, "TC-TS-006 总预算 30s = 600 片(片长上限恒 50ms)"); +} + +/* ===================== TC-TS-010..015:open_listener ===================== */ + +static void tc_open_listener(void) { + fakes_reset(); + { + int s = time_sync_open_listener(); + CHECK(s == 5, "TC-TS-010 open_listener 成功返回 socket 描述符"); + CHECK((s_socket_calls == 1) && (s_bind_calls == 1) && (s_listen_calls == 1), + "TC-TS-010b socket / bind / listen 各一次"); + CHECK(s_htons_last == (uint16_t)TIME_SYNC_PORT, "TC-TS-010c 端口经 net_htons(8888)"); + CHECK(s_bind_port == net_htons((uint16_t)TIME_SYNC_PORT), "TC-TS-010d sin_port 为网络序"); + CHECK(s_bind_family == (uint16_t)NET_AF_INET, "TC-TS-010e sin_family = NET_AF_INET"); + CHECK(s_delay_calls == 0, "TC-TS-010f 成功路径无退避延时"); + } + + fakes_reset(); + s_bind_seq[0] = -1; + s_bind_seq_n = 1; + { + int s = time_sync_open_listener(); + CHECK(s == 5, "TC-TS-011 bind 失败一次后重试成功"); + CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TS-011b bind 失败关闭该 socket"); + CHECK(s_delay_calls == 40, "TC-TS-011c bind 失败退避 2000ms(40 片)"); + CHECK(g_tstest_err_count == 1, "TC-TS-011d bind 失败留痕 DBG_ERROR"); + } + + fakes_reset(); + s_listen_seq[0] = -1; + s_listen_seq_n = 1; + { + int s = time_sync_open_listener(); + CHECK(s == 5, "TC-TS-012 listen 失败一次后重试成功"); + CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TS-012b listen 失败关闭该 socket"); + CHECK(s_delay_calls == 40, "TC-TS-012c listen 失败退避 2000ms"); + } + + fakes_reset(); + s_socket_default = -1; + { + int s = time_sync_open_listener(); + CHECK(s == -1, "TC-TS-013 socket 连续失败 -> 返回 -1"); + CHECK(s_socket_calls == 5, "TC-TS-013b 重试上限 5 次"); + CHECK(g_tstest_err_count == 5, "TC-TS-013c 每次失败各留痕一条(5 条 DBG_ERROR)"); + CHECK(s_delay_calls == 50, "TC-TS-013d 每次失败退避 500ms(5 × 10 片)"); + } + + fakes_reset(); + s_socket_seq[0] = -1; + s_socket_seq[1] = -1; + s_socket_seq[2] = 9; + s_socket_seq_n = 3; + { + int s = time_sync_open_listener(); + CHECK(s == 9, "TC-TS-014 第 3 次重试成功并返回新描述符"); + CHECK(s_socket_calls == 3, "TC-TS-014b 恰好尝试 3 次"); + CHECK(s_delay_calls == 20, "TC-TS-014c 两次失败退避 500ms × 2"); + } + + fakes_reset(); + s_socket_default = -1; + (void)time_sync_open_listener(); + CHECK(s_beat_calls == 55, "TC-TS-015 每轮重试(5) + 每片延时(50) 各喂一次心跳"); + CHECK(s_beat_id_bad == 0, "TC-TS-015b 心跳位恒为 TASK_WD_TIMESYNC"); +} + +/* ===================== TC-TS-020..029:handle_conn ===================== */ + +static void tc_handle_conn(void) { + uint8_t pkt[16]; + + /* 1) 8 字节一次收齐 */ + fakes_reset(); + make_time_packet(pkt, 0x12345678u); + recv_set(0, 8, pkt, 8, 0U); + s_recv_seq_n = 1; + s_tick = 100U; + time_sync_handle_conn(7); + CHECK(s_set_unix_calls == 1, "TC-TS-020 TIME 包一次收齐 -> 更新墙钟一次"); + CHECK(s_set_unix_last == 0x12345678u, "TC-TS-020b epoch 按大端解析"); + CHECK((s_recv_calls == 1) && (s_recv_len_log[0] == 16), "TC-TS-020c 首次请求 16 字节"); + CHECK(s_recv_to_log[0] == 50U, "TC-TS-020d 单次等待片长 = 50ms"); + CHECK((s_close_calls == 1) && (s_close_socks[0] == 7), "TC-TS-020e 处理完关闭连接"); + CHECK(g_tstest_info_count == 1, "TC-TS-020f 留痕 wall clock updated"); + + /* 2) 分段送达:4 + 4 */ + fakes_reset(); + make_time_packet(pkt, 10u); + recv_set(0, 4, pkt, 4, 10U); + recv_set(1, 4, pkt + 4, 4, 10U); + s_recv_seq_n = 2; + time_sync_handle_conn(3); + CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 10u), "TC-TS-021 分段送达累积后解析正确"); + CHECK(s_recv_calls == 2, "TC-TS-021b 读了两次"); + CHECK((s_recv_len_log[0] == 16) && (s_recv_len_log[1] == 12), + "TC-TS-021c 第二次只请求剩余 12 字节"); + CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U), "TC-TS-021d 每片 50ms(预算充裕)"); + + /* 3) 16 字节一次到达(只取前 8) */ + fakes_reset(); + make_time_packet(pkt, 5u); + recv_set(0, 16, pkt, 16, 0U); + s_recv_seq_n = 1; + time_sync_handle_conn(4); + CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 5u), "TC-TS-022 超长包只取前 8 字节"); + CHECK(s_recv_calls == 1, "TC-TS-022b 一次读够即停"); + + /* 4) 前缀不是 TIME */ + fakes_reset(); + pkt[0] = (uint8_t)'X'; + pkt[1] = (uint8_t)'X'; + pkt[2] = (uint8_t)'X'; + pkt[3] = (uint8_t)'X'; + pkt[4] = 0U; + pkt[5] = 0U; + pkt[6] = 0U; + pkt[7] = 1U; + recv_set(0, 8, pkt, 8, 0U); + s_recv_seq_n = 1; + time_sync_handle_conn(4); + CHECK(s_set_unix_calls == 0, "TC-TS-023 非 TIME 前缀不更新墙钟"); + CHECK(s_close_calls == 1, "TC-TS-023b 仍关闭连接"); + + /* 5) 对端关闭(EOF) */ + fakes_reset(); + recv_set(0, 0, NULL, 0, 0U); + s_recv_seq_n = 1; + time_sync_handle_conn(6); + CHECK((s_set_unix_calls == 0) && (s_recv_calls == 1), "TC-TS-024 对端关闭(EOF)立即退出不解析"); + CHECK(s_close_calls == 1, "TC-TS-024b 关闭连接"); + + /* 6) 真错误 */ + fakes_reset(); + recv_set(0, -1, NULL, 0, 0U); + s_recv_seq_n = 1; + time_sync_handle_conn(6); + CHECK((s_recv_calls == 1) && (s_set_unix_calls == 0), "TC-TS-025 真错误立即退出不解析"); + + /* 7) 4 字节后 EOF */ + fakes_reset(); + make_time_packet(pkt, 7u); + recv_set(0, 4, pkt, 4, 0U); + recv_set(1, 0, NULL, 0, 0U); + s_recv_seq_n = 2; + time_sync_handle_conn(6); + CHECK(s_set_unix_calls == 0, "TC-TS-026 不足 8 字节不解析"); + CHECK(s_recv_calls == 2, "TC-TS-026b 继续等待后续字节"); + + /* 8) 30s 预算耗尽 */ + fakes_reset(); + recv_set(0, -2, NULL, 0, 30000U); + s_recv_seq_n = 1; + time_sync_handle_conn(6); + CHECK(s_recv_calls == 1, "TC-TS-027 预算耗尽后不再读(只读 1 次)"); + CHECK(g_tstest_info_count == 1, "TC-TS-027b 留痕 no TIME packet in 30s, drop conn"); + CHECK(s_close_calls == 1, "TC-TS-027c 仍关闭连接"); + + /* 9) 剩余预算不足一片 */ + fakes_reset(); + recv_set(0, -2, NULL, 0, 29980U); + s_recv_seq_n = 1; + time_sync_handle_conn(6); + CHECK(s_recv_calls == 2, "TC-TS-028 两次等待后结束"); + CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 20U), + "TC-TS-028b 第二次片长 = 剩余 20ms"); + + /* 10) 32 位 tick 回绕 */ + fakes_reset(); + s_tick = 0xFFFFFF00U; + recv_set(0, -2, NULL, 0, 512U); /* 推进后回绕到 0x00000100 */ + s_recv_seq_n = 1; + time_sync_handle_conn(6); + CHECK((s_recv_calls == 2) && (s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U), + "TC-TS-029 32 位 tick 回绕后预算计算仍正确"); +} + +/* ===================== TC-TS-040..044:time_sync_task ===================== */ + +static void tc_task(void) { + uint8_t pkt[16]; + + /* A) 无就绪屏障(sys_evtHandle == NULL):accept 轮询 2 次失败后成功 */ + fakes_reset(); + make_time_packet(pkt, 9u); + recv_set(0, 8, pkt, 8, 0U); + s_recv_seq_n = 1; + s_accept_seq[0] = -1; + s_accept_seq[1] = -1; + s_accept_seq[2] = 7; + s_accept_seq_n = 3; + /* gen 调用序:iter1 顶部(1) + accept 两次失败各一次(2,3) + iter2 顶部(4) → 第 4 次跳出 */ + s_gen_longjmp_at = 4; + if (setjmp(s_jmp) == 0) { + time_sync_task(NULL); + } + CHECK((s_wd_bind_calls == 1) && (s_wd_bind_id_bad == 0), + "TC-TS-040 任务入口登记心跳位(task_wd_bind 一次)"); + CHECK(s_evt_calls == 0, "TC-TS-040b sys_evtHandle==NULL 时跳过就绪屏障"); + CHECK(s_accept_calls == 3, "TC-TS-040c accept 轮询 3 次(2 次无连接 + 1 次成功)"); + CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 9u), "TC-TS-040d 收到时间包并更新墙钟"); + CHECK(s_close_calls == 1, "TC-TS-040e 连接被关闭一次"); + CHECK(s_delay_calls == 9, "TC-TS-040f 延时片数 = 4+4+1(200+200+50ms)"); + CHECK(delays_all(50U, 9), "TC-TS-040g 所有延时均按 50ms 分片"); + CHECK(s_beat_calls == 13, "TC-TS-040h 心跳次数 = 主循环1 + 退避8 + 收连接1 + 收尾1 + 次轮1"); + CHECK(s_beat_id_bad == 0, "TC-TS-040i 所有心跳均打 TIMESYNC 位"); + + /* B) 就绪屏障:2 次未就绪 + 1 次就绪 */ + fakes_reset(); + sys_evtHandle = (osEventFlagsId_t)0x55; + s_evt_seq[0] = osFlagsErrorTimeout; + s_evt_seq[1] = osFlagsErrorTimeout; + s_evt_seq[2] = SYS_EVT_NET_READY; + s_evt_seq_n = 3; + s_gen_longjmp_at = 1; + if (setjmp(s_jmp) == 0) { + time_sync_task(NULL); + } + CHECK(s_evt_calls == 3, "TC-TS-041 就绪屏障轮询 3 次(2 次未就绪 + 1 次就绪)"); + CHECK(s_evt_last_timeout == 100U, "TC-TS-041b 屏障等待按 100ms 分片"); + CHECK(s_evt_last_flags == SYS_EVT_NET_READY, "TC-TS-041c 等待 NET_READY 位"); + CHECK(s_evt_last_options == (osFlagsWaitAny | osFlagsNoClear), "TC-TS-041d 就绪位不被消费"); + CHECK(s_beat_calls == 3, "TC-TS-041e 未就绪的两次各喂一次心跳(+主循环 1 次)"); + CHECK(s_wd_bind_calls == 1, "TC-TS-041f 仍先登记心跳路由"); + + /* C) 屏障期间持续喂心跳(就绪位长期不置位) */ + fakes_reset(); + sys_evtHandle = (osEventFlagsId_t)0x55; + s_evt_default = osFlagsErrorTimeout; + s_evt_longjmp_at = 4; + if (setjmp(s_jmp) == 0) { + time_sync_task(NULL); + } + CHECK(s_evt_calls == 4, "TC-TS-042 屏障等待 4 次均未就绪(循环未挂死)"); + CHECK(s_beat_calls == 3, "TC-TS-042b 每次未就绪后各喂一次心跳(第 4 次跳出前)"); + + /* D) 网络代次变化 -> 关闭旧监听并重监听 */ + fakes_reset(); + s_gen_seq[0] = 1u; + s_gen_seq[1] = 2u; + s_gen_seq_n = 2; + s_gen_default = 2u; + s_gen_longjmp_at = 4; + if (setjmp(s_jmp) == 0) { + time_sync_task(NULL); + } + CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TS-043 代次变化时关闭旧监听"); + /* INFO 三条:两次 open_listener 成功各一条 "TCP listen on" + 一条 "network re-init, relisten" */ + CHECK(g_tstest_info_count == 3, "TC-TS-043b 留痕 network re-init, relisten(另含 2 条 listen 成功)"); + CHECK(s_accept_calls == 2, "TC-TS-043c 每次循环只轮询一次 accept 即放弃"); + CHECK((s_socket_calls == 2) && (s_bind_calls == 2) && (s_listen_calls == 2), + "TC-TS-043d 第二次循环重建了监听"); + + /* E) 监听建立失败 -> 5 次重试后放弃并退避 2000ms */ + fakes_reset(); + s_socket_default = -1; + s_gen_longjmp_at = 2; + if (setjmp(s_jmp) == 0) { + time_sync_task(NULL); + } + CHECK(s_socket_calls == 5, "TC-TS-044 open_listener 内部重试 5 次后放弃"); + CHECK(g_tstest_err_count == 6, "TC-TS-044b 5 条 net_socket FAIL + 1 条 retry later"); + CHECK(s_delay_calls == 90, "TC-TS-044c 退避 5×500ms + 2000ms = 90 片"); + CHECK(s_beat_calls == 97, "TC-TS-044d 心跳次数 = 2 + 5 + 50 + 40"); + CHECK(s_beat_id_bad == 0, "TC-TS-044e 所有心跳均打 TIMESYNC 位"); +} + +/* ===================== main ===================== */ + +int main(void) { + printf("=== Time Sync Tests ===\n"); + + printf("-- delay_fed (heartbeat-fed slicing) --\n"); + tc_delay_fed(); + printf("-- open_listener (retry / backoff) --\n"); + tc_open_listener(); + printf("-- handle_conn (packet reassembly / budget) --\n"); + tc_handle_conn(); + printf("-- time_sync_task (barrier / accept / relisten) --\n"); + tc_task(); + + printf("\n=== Time Sync Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/vol_lock/run_unit_test.py b/test/host/vol_lock/run_unit_test.py new file mode 100644 index 00000000..17db483c --- /dev/null +++ b/test/host/vol_lock/run_unit_test.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""卷锁(vol_lock)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/vol_lock/vol_lock_test.c(它以 #include 纳入 + **生产源文件** App/fs/vol_lock.c),在 PC 上离线跑创建/幂等/等待/超时/ + **fail-open**/释放,失败即非 0 退出。零硬件、零烧录、毫秒级。 + + 工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness + `test/host/common/unit_test_harness.py`(本文件只做声明)。 + +为什么要主机侧单测(而不是只用板端套件): + 本模块最要紧的语义是 **fail-open**——锁创建失败时 vol_lock_wait 必须返回 1 + 且**绝不阻塞**(defaultTask 是看门狗喂狗点,等锁 = 停喂 = IWDG 误复位)。 + 真机上 osMutexNew 失败几乎不可复现(需堆耗尽),板端套件无法构造;主机侧 + 一次钉死。幂等性 / 超时透传 / 未创建时 release 无害 同属纯状态逻辑。 + +用法(仓库根目录): + python test/host/vol_lock/run_unit_test.py + 或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/vol_lock/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:App/fs/vol_lock.h 文件头「使用约定(铁律)」; +创建日期:2026-10-10 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +# harness 位于 test/host/common/ +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 顺序敏感:stub 目录必须**先于**生产目录,才能覆盖 cmsis_os.h +# (真实头在 Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/,会拖入整套 +# FreeRTOS)。vol_lock.h 用真实头(接口契约原汁);三个 osMutex* 由测试单元给强符号桩。 +BUILD = TestBuild( + name="Volume Lock Tests", + src=os.path.join(HERE, "vol_lock_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "App", "fs"), + HERE, + ], + summary_marker="=== Volume Lock Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/vol_lock/stub/cmsis_os.h b/test/host/vol_lock/stub/cmsis_os.h new file mode 100644 index 00000000..51f1bc3a --- /dev/null +++ b/test/host/vol_lock/stub/cmsis_os.h @@ -0,0 +1,56 @@ +/* + * 主机侧单测桩:cmsis_os.h(CMSIS-RTOS v1 兼容层) + * 模块功能:为 App/fs/vol_lock.c 提供主机可编译的 CMSIS-RTOS 互斥量接口。 + * + * ⚠ 仅供 test/host/vol_lock/ 使用,不参与 MCU 构建。 + * 真实头来自 Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h, + * 会拖入整套 FreeRTOS(FreeRTOS.h / task.h / queue.h / semphr.h …),主机侧不引入。 + * + * 保真点(对齐 cmsis_os.h + cmsis_os2.h): + * - osMutexId_t = void*(cmsis_os2.h:198); + * - osMutexNew(const osMutexAttr_t *attr)(cmsis_os2.h:552); + * - osMutexWait(osMutexId_t, uint32_t) —— 真实头是 `#define osMutexWait osMutexAcquire`, + * 两者签名一致(cmsis_os.h:617); + * - osMutexRelease(osMutexId_t)(cmsis_os.h:626); + * - osStatus_t 枚举含 osOK = 0(cmsis_os.h:204)。 + * 三个接口由测试单元给强符号桩(可注入"创建失败 / 等待超时")。 + * + * 创建日期:2026-10-10 + */ + +#ifndef CMSIS_OS_H +#define CMSIS_OS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 不透明句柄(真实定义见 cmsis_os2.h:195/198) */ +typedef void *osMutexId_t; +typedef void *osEventFlagsId_t; + +/* 状态码(cmsis_os.h:204 起的枚举;此处只保留被测代码用到的子集) */ +typedef enum { + osOK = 0, /* 成功 */ + osError = -1, /* 通用错误 */ + osErrorTimeout = -2, /* 超时 */ + osErrorResource = -3, /* 资源不可用 */ + osErrorParameter = -4 /* 参数错误 */ +} osStatus_t; + +/* 互斥量属性:被测代码只传 NULL,故仅占位 */ +typedef struct { + uint32_t attr_bits; +} osMutexAttr_t; + +osMutexId_t osMutexNew(const osMutexAttr_t *attr); +osStatus_t osMutexWait(osMutexId_t mutex_id, uint32_t millisec); +osStatus_t osMutexRelease(osMutexId_t mutex_id); + +#ifdef __cplusplus +} +#endif + +#endif /* CMSIS_OS_H */ diff --git a/test/host/vol_lock/vol_lock_test.c b/test/host/vol_lock/vol_lock_test.c new file mode 100644 index 00000000..5c48416d --- /dev/null +++ b/test/host/vol_lock/vol_lock_test.c @@ -0,0 +1,224 @@ +/* + * 模块名称:Volume Lock(卷锁)主机侧单元测试 + * 模块功能:在 PC 上对 App/fs/vol_lock.c 做离线验证(3 个公开函数全覆盖)。 + * + * 被测对象:真实源文件 ../../../App/fs/vol_lock.c(以 #include 原样纳入, + * 保证测的是生产代码而非副本)。平台依赖由本文件提供的**可注入桩**实现: + * - cmsis_os.h -> osMutexNew / osMutexWait / osMutexRelease + * (可注入 创建失败 / 等待超时 / 获取成功) + * + * 为什么要主机侧单测(而不是只用板端套件): + * 1. 本模块最要紧的语义是 **fail-open**(锁创建失败时 `vol_lock_wait` 必须返回 1 + * 且**绝不阻塞**)——这是"defaultTask 喂狗点绝不能因等锁而停摆"的最后一道保险。 + * 真机上 `osMutexNew` 失败几乎不可能复现(需堆耗尽),板端套件无法构造。 + * 2. 幂等性(重复 `vol_lock_init` 不得重复创建)、等待超时透传、未创建时 release 无害, + * 都是纯状态逻辑,主机侧一次钉死。 + * 3. 不占 MCU 的 RAM,毫秒级完成。 + * + * 覆盖点: + * TC-VL-001 init 首次调用创建互斥量并返回 0 + * TC-VL-002 init 幂等(重复调用不再创建) + * TC-VL-003 init 在 osMutexNew 失败时返回 -1 + * TC-VL-004 init 失败后可重试成功(s_lock 未被污染) + * TC-VL-005 wait 成功后返回 1,且把 timeout 原样透传给 osMutexWait + * TC-VL-006 wait 超时(osMutexWait != osOK)返回 0 + * TC-VL-007 wait 在锁未创建时自动 init,再正常获取 + * TC-VL-008 ⭐ fail-open:锁未创建且创建失败 -> 返回 1 且**不调用** osMutexWait + * TC-VL-009 fail-open 之后 release 无害(s_lock 仍为 NULL,不调用 osMutexRelease) + * TC-VL-010 已创建时 wait 不再调用 osMutexNew + * TC-VL-011 release 在已持锁时调用 osMutexRelease 一次 + * TC-VL-012 release 在锁未创建时静默返回(不崩溃、不调用) + * TC-VL-013 完整周期 init -> wait -> release -> wait -> release + * TC-VL-014 wait(0) 的 timeout 透传为 0(defaultTask 的 try 语义) + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-10 + */ + +#include +#include +#include + +/* ===================== 平台桩(先纳入,覆盖真实头) ===================== */ +#include "cmsis_os.h" + +/* ===================== CMSIS-RTOS 互斥量桩 ===================== + * s_new_fail osMutexNew 返回 NULL(注入创建失败) + * s_wait_ok 0 -> osMutexWait 返回 osErrorTimeout(注入超时) + * s_fake_mutex 成功创建时返回的伪句柄(非 NULL 即可) + * s_last_timeout 记录 osMutexWait 收到的 millisec(断言透传) + */ +static int s_new_calls; +static int s_new_fail; +static int s_wait_calls; +static int s_wait_ok; +static int s_release_calls; +static uint32_t s_last_timeout; +static void *s_fake_mutex = (void *)0x1234; + +osMutexId_t osMutexNew(const osMutexAttr_t *attr) { + (void)attr; + s_new_calls++; + return s_new_fail ? NULL : (osMutexId_t)s_fake_mutex; +} + +osStatus_t osMutexWait(osMutexId_t mutex_id, uint32_t millisec) { + (void)mutex_id; + s_wait_calls++; + s_last_timeout = millisec; + return s_wait_ok ? osOK : osErrorTimeout; +} + +osStatus_t osMutexRelease(osMutexId_t mutex_id) { + (void)mutex_id; + s_release_calls++; + return osOK; +} + +/* ===================== 断言 ===================== */ +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 "vol_lock.c" + +/* ===================== 工具 ===================== */ + +/* + * 函数功能:把全部桩与模块静态态复位到"刚上电"初值 + * 入口参数:无 + * 返 回 值:无 + * 限定条件:无 + * 函数说明:`s_lock` 是 vol_lock.c 的文件静态量(源文件已被纳入同一编译单元), + * 直接置 NULL 等价于"未初始化过"——这是白盒单测的可接受代价。 + */ +static void fakes_reset(void) { + s_new_calls = 0; + s_new_fail = 0; + s_wait_calls = 0; + s_wait_ok = 1; + s_release_calls = 0; + s_last_timeout = 0xFFFFFFFFu; + s_fake_mutex = (void *)0x1234; + s_lock = NULL; +} + +/* ===================== TC-VL-001..004:vol_lock_init ===================== */ + +static void tc_init(void) { + fakes_reset(); + CHECK(vol_lock_init() == 0, "TC-VL-001 init 首次调用返回 0"); + CHECK(s_new_calls == 1, "TC-VL-001b init 创建互斥量一次(osMutexNew)"); + CHECK(s_lock != NULL, "TC-VL-001c 模块静态态已持有句柄"); + + CHECK(vol_lock_init() == 0, "TC-VL-002 init 重复调用仍返回 0(幂等)"); + CHECK(s_new_calls == 1, "TC-VL-002b 幂等:不再创建第二个互斥量"); + + fakes_reset(); + s_new_fail = 1; + CHECK(vol_lock_init() == -1, "TC-VL-003 osMutexNew 失败 -> init 返回 -1"); + CHECK(s_lock == NULL, "TC-VL-003b 失败时不留下半初始化句柄"); + + s_new_fail = 0; + CHECK(vol_lock_init() == 0, "TC-VL-004 失败后重试可成功(返回 0)"); + CHECK(s_new_calls == 2, "TC-VL-004b 重试确实再次调用 osMutexNew"); +} + +/* ===================== TC-VL-005..010:vol_lock_wait ===================== */ + +static void tc_wait(void) { + /* --- 已创建:超时值原样透传 --- */ + fakes_reset(); + (void)vol_lock_init(); + CHECK(vol_lock_wait(100U) == 1, "TC-VL-005 wait 获取成功返回 1"); + CHECK(s_wait_calls == 1, "TC-VL-005b osMutexWait 被调用一次"); + CHECK(s_last_timeout == 100U, "TC-VL-005c timeout 原样透传给 osMutexWait"); + + fakes_reset(); + (void)vol_lock_init(); + CHECK(vol_lock_wait(0U) == 1, "TC-VL-014 wait(0) 获取成功返回 1(try 语义)"); + CHECK(s_last_timeout == 0U, "TC-VL-014b wait(0) 的 timeout 透传为 0"); + + /* --- 超时:osMutexWait != osOK -> 返回 0 --- */ + fakes_reset(); + (void)vol_lock_init(); + s_wait_ok = 0; + CHECK(vol_lock_wait(50U) == 0, "TC-VL-006 osMutexWait 非 osOK -> wait 返回 0"); + CHECK(s_wait_calls == 1, "TC-VL-006b 超时路径确实尝试过等待"); + + /* --- 锁未创建:自动 init 后正常获取 --- */ + fakes_reset(); + CHECK(vol_lock_wait(10U) == 1, "TC-VL-007 锁未创建时 wait 自动 init 并获取"); + CHECK(s_new_calls == 1, "TC-VL-007b 自动 init 创建了互斥量"); + CHECK(s_wait_calls == 1, "TC-VL-007c 随后执行了一次等待"); + + /* --- ⭐ fail-open:创建失败必须返回 1 且绝不阻塞 --- */ + fakes_reset(); + s_new_fail = 1; + CHECK(vol_lock_wait(0U) == 1, "TC-VL-008 fail-open:锁创建失败仍返回 1(不阻塞喂狗点)"); + CHECK(s_wait_calls == 0, "TC-VL-008b fail-open 时**不调用** osMutexWait(零阻塞)"); + CHECK(s_lock == NULL, "TC-VL-008c fail-open 后仍无句柄"); + + /* --- fail-open 之后 release 无害 --- */ + vol_lock_release(); + CHECK(s_release_calls == 0, "TC-VL-009 fail-open 后 release 不调用 osMutexRelease(判空静默)"); + + /* --- 已创建时 wait 不再创建 --- */ + fakes_reset(); + (void)vol_lock_init(); + (void)vol_lock_wait(0U); + (void)vol_lock_wait(0U); + CHECK(s_new_calls == 1, "TC-VL-010 多次 wait 不重复创建互斥量"); + CHECK(s_wait_calls == 2, "TC-VL-010b 每次 wait 各等待一次"); +} + +/* ===================== TC-VL-011..013:vol_lock_release ===================== */ + +static void tc_release(void) { + /* --- 已持锁:释放一次 --- */ + fakes_reset(); + (void)vol_lock_init(); + (void)vol_lock_wait(0U); + vol_lock_release(); + CHECK(s_release_calls == 1, "TC-VL-011 已持锁时 release 调用 osMutexRelease 一次"); + + /* --- 锁未创建:静默返回 --- */ + fakes_reset(); + vol_lock_release(); + CHECK(s_release_calls == 0, "TC-VL-012 锁未创建时 release 静默返回(不调用、不崩溃)"); + + /* --- 完整周期 --- */ + fakes_reset(); + CHECK(vol_lock_init() == 0, "TC-VL-013 周期:init -> 0"); + CHECK(vol_lock_wait(0U) == 1, "TC-VL-013b 周期:wait -> 1"); + vol_lock_release(); + CHECK(vol_lock_wait(100U) == 1, "TC-VL-013c 周期:再 wait -> 1(可重复获取)"); + vol_lock_release(); + CHECK(s_new_calls == 1, "TC-VL-013d 周期:全程只创建一次互斥量"); + CHECK(s_release_calls == 2, "TC-VL-013e 周期:释放两次"); + CHECK(s_last_timeout == 100U, "TC-VL-013f 周期:第二次 wait 的 timeout 透传正确"); +} + +/* ===================== main ===================== */ + +int main(void) { + printf("=== Volume Lock Tests ===\n"); + + printf("-- vol_lock_init --\n"); + tc_init(); + printf("-- vol_lock_wait (incl. fail-open) --\n"); + tc_wait(); + printf("-- vol_lock_release --\n"); + tc_release(); + + printf("\n=== Volume Lock Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/watchdog/run_unit_test.py b/test/host/watchdog/run_unit_test.py index 1922f12e..0fb5e4bf 100644 --- a/test/host/watchdog/run_unit_test.py +++ b/test/host/watchdog/run_unit_test.py @@ -22,7 +22,7 @@ 退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 -设计依据:docs/测试说明/看门狗单元测试说明.md;docs/待处理内容.md T-3 +设计依据:docs/测试说明/看门狗单元测试说明.md;docs/任务规划/登记簿.md T-3 创建日期:2026-10-01 """ import os diff --git a/test/host/watchdog/watchdog_test.c b/test/host/watchdog/watchdog_test.c index b689f6f1..4df4bd3a 100644 --- a/test/host/watchdog/watchdog_test.c +++ b/test/host/watchdog/watchdog_test.c @@ -1,4 +1,4 @@ -/* +/* * 模块名称:Task Watchdog 主机侧单元测试 * 模块功能:在 PC 上对 App/util/task_watchdog.c 的**判定状态机**做穷举验证。 * @@ -116,7 +116,7 @@ static void start_and_clear(void) { s_feed_count = 0; } -/* 全员置位(默认必需掩码 0x77 = bit0|1|2|4|5|6,含 adcTask) */ +/* 全员置位(默认必需掩码 0x1F7 = bit0|1|2|4|5|6|7|8,含 adc/iec104/protect) */ static void beat_all(void) { task_wd_beat(TASK_WD_DEFAULT); task_wd_beat(TASK_WD_NET); @@ -124,6 +124,8 @@ static void beat_all(void) { task_wd_beat(TASK_WD_UI); task_wd_beat(TASK_WD_TIMESYNC); task_wd_beat(TASK_WD_ADC); + task_wd_beat(TASK_WD_IEC104); + task_wd_beat(TASK_WD_PROTECT); } /* ===================== 用例 ===================== */ @@ -152,7 +154,7 @@ static void tc_003_all_beat_feeds(void) { beat_all(); poll_advance(100); CHECK(s_feed_count == 1, "全员到位喂狗 1 次"); - CHECK(task_wd_last_mask() == 0x77u, "last_mask == 0x77(bit0|1|2|4|5|6)"); + CHECK(task_wd_last_mask() == 0x1F7u, "last_mask == 0x1F7(bit0|1|2|4|5|6|7|8)"); } static void tc_004_take_and_clear(void) { @@ -174,9 +176,11 @@ static void tc_005_mask_required(void) { task_wd_beat(TASK_WD_FTP); task_wd_beat(TASK_WD_TIMESYNC); task_wd_beat(TASK_WD_ADC); + task_wd_beat(TASK_WD_IEC104); + task_wd_beat(TASK_WD_PROTECT); poll_advance(100); CHECK(s_feed_count == 0, "缺 UI 位 → 不喂狗"); - CHECK(task_wd_last_mask() == (0x77u & ~(1u << TASK_WD_UI)), + CHECK(task_wd_last_mask() == (0x1F7u & ~(1u << TASK_WD_UI)), "last_mask 精确记录缺失位"); } @@ -252,7 +256,7 @@ static void tc_011_observers(void) { CHECK(task_wd_is_armed() == 1, "is_armed == 1(已启动)"); beat_all(); poll_advance(100); - CHECK(task_wd_last_mask() == 0x77u, "last_mask 反映最近一次抽样"); + CHECK(task_wd_last_mask() == 0x1F7u, "last_mask 反映最近一次抽样"); CHECK(task_wd_miss_count() == 0u, "全程达标 → miss_count == 0"); } diff --git a/test/run_all_suites.py b/test/run_all_suites.py index 0a579913..15227338 100644 --- a/test/run_all_suites.py +++ b/test/run_all_suites.py @@ -40,7 +40,7 @@ ② 防止"某套件长期编不过而无人知"(2026-08~09 CH395F 套件实例)。 注意 RAM 溢出(P1-9)会自然暴露为链接错误 L6406E——这正是想要的。 -设计依据:docs/待处理内容.md T-5;docs/测试说明/测试开发标准流程.md §3.1 +设计依据:docs/任务规划/登记簿.md T-5;docs/测试说明/测试开发标准流程.md §3.1 创建日期:2026-10-01 """