Files
STM32F4-Base/scripts/check_registration.py
T
wandering1 0264b49489 feat(algo): B8 频率测量层 + 过频/低频保护(L3-5)
新增 App/algo/freq.c/h:由 L2 基波相量导出系统频率。相邻两拍共轭
乘积 P=Z(m)·conj(Z(m-1)) 取角 = 每拍相位增量(免显式解缠绕),再按
自适应窗长 K_eff=round(8·Fs/(2·f_hat))(45~55 Hz 段 73~89 拍)做滑动
平均。定长窗受 20 点矩形窗负序泄漏纹波限制(实测扫频最坏 0.317 Hz;
60 拍 Kay 复数和 0.197 Hz,交叉项和角不按 1/K^2 衰减),自适应窗使
窗长恰为纹波周期 fs/(2f) 整数倍 -> 最坏 0.0101 Hz。推导与两轮弯路
写在 freq.c 文件头。

假值钳制带取 35~65 Hz,刻意宽于系统合理带 40.5~60.5:单拍相位差本身
带 ±5 Hz 泄漏纹波,贴边钳制会非对称丢弃纹波半周 -> 均值偏置(实测
45 Hz 点 +0.16 Hz)。系统合理带由 OF/UF 定值范围在消费侧把关。
幅值门 FREQ_AMP_MIN=50 码(拟定,待 L1-5 标定复核):低于门判无有效
波形,停更新、valid 清零、读数保持 -> 浮空/PT 断线不发散。源优先级
UA(C02)->IA(C01),按名前缀解析、每拍动态回退,切换即废相位基准。

prot_set:尾追加 8 项(PS_ID_COUNT 32->40,编号不回收)
OF_EN/OF_HZ/OF_DLY/UF_EN/UF_HZ/UF_DLY/RET_OF/RET_UF。OF/UF 逐项通讯
地址不在本仓库 -> 全部 comm_addr=0 本地项(104 C_SE 不可寻址,待对照
参考表补)。频率返回系数独立(0.99/1.01),勿与电气量 0.95/1.05 共用。
bad_mask 随项数扩为 uint64_t。PROT_SET_BLOB_VER 1->2。

prot_set_store:换代与损坏分流。load 返回 2 = 版本/项数换代(本文件
打 INFO "prot set blob version change, defaults in use"),-1 = 真损坏
(defaultTask 按 CORRUPT 报 ERROR + 自检留痕)。B7 旧档(ver=1/32 项)
升级首启属预期路径,不得报成损坏。语义分流收在属主文件内,
default_task.c 不动。

prot_elm:实例表加 src 字段(0=phasor rms,1=freq),PE_OF/PE_UF 尾
追加(PE_COUNT 7->9)。freq 无有效读数按方向填"必不启动"值(过量 0 /
欠量 1e30 -> 滞回比较恒假 -> 元件复位),不整拍跳过——频率源缺失不得
饿死电流/电压类元件(B5 行为不回归)。OF/UF 暂未映射独立 104 遥信点,
动作只经 YX15 保护总聚合可见(送检点表 = B10)。

板端接线:adcTask 在 phasor_run() 之后同拍调 freq_run();心跳/首拍日志
追加 freq(<tag>): f=%ld valid=%u (x0.01)(MicroLIB 无 %f)。app_main
内核前 freq_init()。

主机单测:freq 46 用例(J1 扫频 45~55 Hz <=0.02 Hz 实测最坏 0.0101;
J2 噪声 1% <=0.10 实测 0.0160;J3 启动收敛 <=40 ms 实测 20;J4 阶跃
50->51 <=120 ms 实测 98;J5 浮空 valid=0 不发散;J6 跳变 60 ms 内回
±0.05;另直流 8000 码 / 3-5 次谐波 20%/10% / UA->IA 回退)。
prot_set 90(+TC-PS-011 OF/UF 行 + 64 位 bad_mask bit39 + 旧档拒绝)、
prot_elm 47(+OF/UF 滞回与方向隔离 + run() 测频接线)。回归:phasor
57、iec104 94 全绿。

J4=120 ms 是"精度优先于响应"的显式取舍(Lead 批准):4 周波窗换
0.317->0.0101 Hz,测频响应 98 ms << OF/UF 定时限默认 1000 ms(占
总动作时间 <10%),暂态另有 J6 <=60 ms 兜底。

注册:freq.c 入 uvprojx algo 组 + check_registration.py SIM_EXEMPT
(纯算法、模拟器无消费路径,照 prot_set.c 先例)。

@build 0 错误 0 警告(Code=207468 RO=18304 RW=1068 ZI=153084)。
RAM:RW_IRAM1 96096->96728 B(+632 B,73.3%->73.8%)、RW_CCM 不变
57376 B(87.9%)、Flash 224596->226840 B(+2244 B,21.4%->21.6%);
台账随合并由 Lead 回填。stack_report:adcTask 静态最坏链 1320 B /
余量 1240 B(48%)改前后完全不变(freq_run 帧未进最坏链,无需抬栈);
protectTask 链 872 B / 余 1176 B(57%)同样不变。

gate: only mem-budget/mem-numbers pending lead ledger backfill
--------------------------------------------------------------
门禁实跑(pre-commit run --all-files,Lead 批准 --no-verify 提交):
28 Passed / 2 Failed,失败仅 mem-numbers 与 mem-budget 两条——均为
docs/内存管理 受管快照与本 .map 不一致(台账由 Lead 每次合并后
--write 回填,避免多分支同改受管表冲突)。其余含 doc-links /
coding-standard / no-mojibake / cppcheck / registration-check /
13 条主机单测钩子全 Passed(gitlint 属 commit-msg 阶段,不在该次
--all-files 内)。ci_local L0.5 全绿(freq 46/46 等)。
字节卫生自查(--no-verify 跳过卫生钩子,故手工核):17 个暂存文件
全部 BOM=无 / CR=0(纯 LF)/ U+FFFD=0;自研 .c/.h 去注释后字符串
字面量 100% ASCII(唯一非 ASCII 字面量在 test/host/iec104/iec104_test.c
的既有用例名,非本次改动行;test/ 不在 coding-standard 钩子范围)。
2026-10-12 19:16:37 +08:00

309 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""check_registration.py - 双端注册反向校验门禁(纯标准库)。
背景(登记簿 N-23,出处审查报告 N-06 残余):
本工程 Keil 工程与 PC 模拟器都用**显式文件列表**(无通配扫描)。新 `.c`
必须手动注册两处才会被编译:
① `MDK-ARM/STM32F407-Demo.uvprojx` 的 `<FilePath>` 条目(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/algo/freq.c", "L2 派生测频纯算法(输入=phasor 使用区),模拟器无消费路径"),
("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/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 的 <FilePath>,返回仓库相对 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"<FilePath>(.*?)</FilePath>", 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())