From 64d8b052b1ab41ced8c08efcea5e5c3c363d30e5 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Wed, 21 Oct 2026 11:15:00 +0800 Subject: [PATCH] =?UTF-8?q?docs(algo):=20App/algo/README=20=E7=BC=A9?= =?UTF-8?q?=E6=88=90=E7=9B=AE=E5=BD=95=E7=B4=A2=E5=BC=95=EF=BC=8C=E8=AF=A6?= =?UTF-8?q?=E7=BB=86=E8=AE=BE=E8=AE=A1=E8=BF=81=E5=85=A5=20docs/=E7=AE=97?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 拆分层级 - 新建 docs/算法/采集与算法层设计说明.md(413 行、13 节)承接机制、契约、 判据与设计理由:定位与边界 / 数据流与初始化落点 / adc_input / adc_health / sample_ring / phasor / freq(7.1 为什么自适应窗、7.2 判据 J1~J6)/ prot_set / prot_elm / 关键约定与禁令 / 资源足迹 / 验证与判读 / 相关文档 - App/algo/README.md 360 -> 128 行:只留文件清单、依赖、逐文件"改代码必须 知道"的红线、构建与测试入口、新增模块动作;深挖改成逐条指针 - README 保留 §3.1-§3.7 原编号,既有"§3.7"引用不失效 索引与引用同步 - 索引:docs/README.md §5.2 新增行;项目架构总览主题表拆成"设计与判据 / 目录索引"两行;AGENTS.md 与 App/README.md 指向(后者原文还写着 L1/L2) - 按内容重指(编号仍在但内容已搬走,只核编号会静默指错):App/algo/freq.h、 test/host/freq/freq_test.c、run_unit_test.py、继电保护规划 L2-4 行、 pre-commit使用说明 J4 行 -> 指向设计说明 - ADC采样节拍设计说明的两处指向同步改到新文档 修 README 内过时事实 - 挂载写"八个 .c",实际 uvprojx 内 7 个 - SIM_EXEMPT 写"prot_set / freq 才登记豁免",实际七个全在表内 - freq-unit-test 标"待登记",实际已在 .pre-commit-config.yaml - 定值项数一处 31->40、一处 32->40;blob 144->176 B 证明是 32->40 - 用例与判据数字不再三处复制:属主是 test/host/<模块>/ 与 docs/测试说明/ 门禁 - doc-links --all 无新增问题;--sections-strict 覆盖 10 篇文档通过 - id-check 可判定 82.65%(基线 82.6,容差 0.1);TC 双向追溯差异 124 处不变 - registration-check PASS(1 条已知 WARN);freq 单测 46/46;@build 0 错 0 警 - 全部改动文件无 BOM、无 CR、U+FFFD = 0 --- AGENTS.md | 2 +- App/README.md | 2 +- App/algo/README.md | 438 ++++++------------------- App/algo/freq.h | 2 +- docs/README.md | 1 + docs/代码规范/pre-commit使用说明.md | 2 +- docs/子系统设计/ADC采样节拍设计说明.md | 4 +- docs/架构设计/项目架构总览.md | 3 +- docs/算法/继电保护功能复现规划.md | 2 +- docs/算法/采集与算法层设计说明.md | 413 +++++++++++++++++++++++ test/host/freq/freq_test.c | 2 +- test/host/freq/run_unit_test.py | 2 +- 12 files changed, 528 insertions(+), 345 deletions(-) create mode 100644 docs/算法/采集与算法层设计说明.md diff --git a/AGENTS.md b/AGENTS.md index e5e937c5..a2963b12 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,7 @@ - netTask:`net_stack_bring_up`(配置已由 defaultTask 在屏障前加载,本任务只读 RAM 副本)→ 置 NET_READY → 循环 `net_poll` + `net_process_messages`;消费 `SYS_EVT_NET_RECONFIG` → 按网络代次号重建监听(新 IP 免重启生效) - defaultTask:`f_mount` forced(无卷自动 `f_mkfs`;挂载后卷健康探针,FAT 区陈旧时自愈重格式化)→ 加载网络配置 → STORAGE_READY → LED 闪烁;同时是 syslog(`/log/run.log`·`selftest.log`,定长 64B 记录)的**唯一写者**与**配置引擎属主**(netTask/uiTask 不得直接触碰配置引擎) - adcTask:**纯消费者**——每拍 `adc_input_poll()`(TPAFE 原子快照 + 消费一帧 STM32 ADC1 DMA 双缓冲),只取数不做保护运算;**节拍与唤醒同源**(TIM3 ISR 内触发 CONVST + 同一更新事件经 TRGO 触发 ADC1,并调 `adc_tick_irq_notify()` 置位,任务 `osThreadFlagsWait` 等唤醒)。⚠ ISR 侧**只能**走 `adc_tick_irq_notify()`(内含"消费者就绪"门,TIM3 比调度器早 ≈100 ms 启动);手写于 `Src/freertos.c` 的 `USER CODE` 区,纳入 IWDG `required_mask`(`TASK_WD_ADC`) -- **采集与算法层(L1/L2/L3)落在 `App/algo/`**:`adc_input`/`adc_health` = L1,`sample_ring`/`phasor` = L2,`freq` = L2.5 测频(B8,自适应窗),`prot_set`/`prot_elm` = L3。板端消费者 = **protectTask**(B5,2026-10-12):20 ms 节拍 `prot_elm_run()` + L5-4 采样故障闭锁 + L5-1 定值校验和巡检;跨上下文复归(104 YK11)经 `prot_elm_request_reset()` 落请求。纳入 IWDG `required_mask`(`TASK_WD_PROTECT`,产品 `0x1F7`)。→ `App/algo/README.md` +- **采集与算法层(L1/L2/L3)落在 `App/algo/`**:`adc_input`/`adc_health` = L1,`sample_ring`/`phasor` = L2,`freq` = L2.5 测频(B8,自适应窗),`prot_set`/`prot_elm` = L3。板端消费者 = **protectTask**(B5,2026-10-12):20 ms 节拍 `prot_elm_run()` + L5-4 采样故障闭锁 + L5-1 定值校验和巡检;跨上下文复归(104 YK11)经 `prot_elm_request_reset()` 落请求。纳入 IWDG `required_mask`(`TASK_WD_PROTECT`,产品 `0x1F7`)。→ `docs/算法/采集与算法层设计说明.md`(设计与判据)、`App/algo/README.md`(目录索引) - 全景 → `docs/架构设计/项目架构总览.md`;启动时序与内核前模式 → `docs/架构设计/项目启动顺序管理.md` diff --git a/App/README.md b/App/README.md index e2b14f6f..b4aef01d 100644 --- a/App/README.md +++ b/App/README.md @@ -16,7 +16,7 @@ FreeRTOS/CMSIS-RTOS API**(见 `AGENTS.md`「平台约束」)。 | `app/` | **应用入口与全局**:`app_main.c/h`(初始化入口)、`app_cfg.h`(编译开关)、`sys_events.h`(就绪事件) | [`app/README.md`](app/README.md) | | `cfg/` | **配置引擎**:`/config.ini` 解析 + 原子持久化(旧二进制档已废弃,2026-10-10) | [`cfg/README.md`](cfg/README.md) | | `sync/` | **时间同步任务**:`time_sync.c/h` | [`sync/README.md`](sync/README.md) | -| `algo/` | **采集与算法层(L1/L2)**:`adc_input.c/h`(语义通道映射 + STM32 ADC1 扫描) | [`algo/README.md`](algo/README.md) | +| `algo/` | **采集与算法层(L1/L2/L3)**:语义取数 + 有效性判定 + 采样环/相量/测频 + 定值模型 + 保护元件框架 | [`algo/README.md`](algo/README.md)(索引);设计 → `docs/算法/采集与算法层设计说明.md` | | `log/` | **日志子系统**:syslog(NAND)+ 控制台单写者日志 | [`log/README.md`](log/README.md) | | `task/` | **FreeRTOS 任务实现**(6 个任务) | [`task/README.md`](task/README.md) | | `util/` | **通用工具**:CRC / 环形缓冲 / 墙钟时间 | [`util/README.md`](util/README.md) | diff --git a/App/algo/README.md b/App/algo/README.md index 76dc63d8..5860f348 100644 --- a/App/algo/README.md +++ b/App/algo/README.md @@ -1,360 +1,128 @@ -# App/algo/ — 采集与算法层(L1 / L2) +# App/algo/ — 采集与算法层(L1 / L2 / L3) -> **自研代码**,属 `docs/算法/继电保护功能复现规划.md` §2.1 分层中的 **L1(采集与预处理)** -> 与 **L2(相量计算)**。原则:**算法与 I/O、与调度分离**——本层不建任务、不做保护判据、 -> 不碰寄存器(唯一例外见 §3.1),只做"数"(原始码值 → 语义通道)与"相量" -> (采样序列 → 复相量),因而可被主机侧单元测试(L0.5)离线全覆盖(落实登记簿 **A-4**)。 -> -> 任务侧(`App/task/adc_task.c`)只做**消费与编排**;任何新算法一律落在本目录,勿塞进 `App/task/`。 +> 本目录的**索引**:文件清单、依赖、逐文件的"改代码必须知道"、构建与测试入口、新增模块动作。 +> **详细设计——机制、契约、判据、为什么这样设计 → `docs/算法/采集与算法层设计说明.md`**(本文不复制其内容)。 +> 分层归属与功能项状态 → `docs/算法/继电保护功能复现规划.md` §2.1; +> 通道映射唯一权威 → `docs/硬件平台/ADC通道映射说明.md`;采样节拍 → `docs/子系统设计/ADC采样节拍设计说明.md`。 ## 1. 文件清单(一眼版) | 文件 | 层 | 作用(一句话) | 主要对外入口 | 覆盖规划项 | |---|---|---|---|---| -| `adc_input.h` / `adc_input.c` | L1 | **统一语义取数层**:把两片转换器的原始码值按语义(`UA…IN`/温度)暴露,并消费 STM32 ADC1 的 DMA 乒乓双缓冲 | `adc_input_init()` `adc_input_poll()` `adc_input_raw()` | L1-2 / L1-3 / L1-4 | -| `adc_health.h` / `adc_health.c` | L1 | **采样有效性判定层**:吃"计数快照"(帧序号/错误计数),产出静默/丢帧/单调性/错误增量故障位——判定与告警编排分离 | `adc_health_init()` `adc_health_check()` `adc_health_flags()` | L1-6 | -| `sample_ring.h` / `sample_ring.c` | L2 输入 | **采样环缓冲**:按语义通道保存最近 2 个周波(40 点)采样值,供相量层"以当前时刻为终止"取窗 | `sample_ring_push_frame()` `sample_ring_window()` | L2 输入(为 L2-6 滑动窗供数) | -| `phasor.h` / `phasor.c` | L2 | **全周傅氏相量计算**:一周波 20 点单频 DFT → 基波/谐波相量(有效值口径),双区原子切换供无锁读 | `phasor_run()` `phasor_get()` `phasor_calc()` | L2-1 / L2-2 / L2-3 / L2-6 / L2-7 | -| `prot_set.h` / `prot_set.c` | L3 输入 | **保护定值模型**:Q10 定点 RAM 表(通讯地址对照参考 `g_tCommSetParaTab`)+ 范围校验 + 校验和封存自检闭锁 | `prot_set_init()` `prot_set_load_raw()` `prot_set_get_f()` `prot_set_selfcheck()` | §5 定值模型 / L5-1 | -| `prot_elm.h` / `prot_elm.c` | L3 | **保护元件框架**:通用"越限 + 返回系数滞回 + 定时限"核 + 数据驱动实例表(过流三段 / 过负荷 / 过压 / 欠压 / 零序过压 / **过频 / 低频**) | `prot_elm_init()` `prot_elm_eval()` `prot_elm_run()` `prot_elm_active_mask()` | L3-1 / L3-3 / L3-4 / L3-5 / L3-6(软件侧) | -| `freq.h` / `freq.c` | L2 派生量 | **频率测量**:相邻两拍基波相量共轭乘积取角(每拍相位增量)+ 自适应窗长滑动平均 + 假值钳制 + 幅值门(UA→IA 源回退) | `freq_init()` `freq_run()` `freq_get()` `freq_valid()` | L3-5 测量侧(`docs/算法/继电保护功能复现规划.md` §4.2 测频范围) | +| `adc_input.h` / `.c` | L1 | 统一语义取数:两片转换器的码值按语义(`UA…IN` / 温度)暴露,并消费 STM32 ADC1 的 DMA 乒乓缓冲 | `adc_input_init()` `adc_input_poll()` `adc_input_raw()` | L1-2 / L1-3 / L1-4 | +| `adc_health.h` / `.c` | L1 | 采样有效性判定:吃计数快照,产出静默 / 丢帧 / 单调性 / 错误增量故障位(判定与告警编排分离) | `adc_health_check()` `adc_health_flags()` | L1-6 | +| `sample_ring.h` / `.c` | L2 输入 | 按语义通道存最近 2 周波(40 点),供相量层"以当前时刻为终止"取窗 | `sample_ring_push_frame()` `sample_ring_window()` | L2 输入(为 L2-6 供数) | +| `phasor.h` / `.c` | L2 | 全周傅氏:一周波 20 点 DFT → 基波 / 谐波相量(有效值口径),双区原子切换供无锁读 | `phasor_run()` `phasor_get()` `phasor_calc()` | L2-1 / L2-2 / L2-3 / L2-6 / L2-7 | +| `freq.h` / `.c` | L2 派生量 | 频率测量:相邻两拍基波相量共轭乘积取角 + 自适应窗滑动平均 + 假值钳制 + 幅值门(UA→IA 源回退) | `freq_run()` `freq_get()` `freq_valid()` | L3-5 测量侧(测频范围 → `docs/算法/继电保护功能复现规划.md` §4.2) | +| `prot_set.h` / `.c` | L3 输入 | 保护定值模型:Q10 定点 RAM 表 + 范围校验 + 校验和封存 / 自检 / 闭锁 + 持久化 blob 纯逻辑 | `prot_set_load_raw()` `prot_set_get_f()` `prot_set_selfcheck()` | L5-1 载体 | +| `prot_elm.h` / `.c` | L3 | 保护元件框架:通用"越限 + 返回系数滞回 + 定时限"核 + 数据驱动实例表(过流三段 / 过负荷 / 过压 / 欠压 / 零序过压 / 过频 / 低频) | `prot_elm_run()` `prot_elm_eval()` `prot_elm_active_mask()` | L3-1 / L3-3 / L3-4 / L3-5 / L3-6(软件侧) | -依赖关系(自下而上,无环):`adc_input.h`(纯枚举 + 声明)← `sample_ring` ← `phasor` ← `freq`; -`prot_set` **独立**(仅 `stdint.h`);`prot_elm` ← `prot_set` + `phasor` + `freq`。 -`sample_ring.c` / `phasor.c` / `adc_health.c` / `prot_set.c` / `prot_elm.c` / `freq.c` **零平台依赖** -(只用 `math.h` / `stddef.h` / `stdint.h` 与本目录头); -`adc_input.c` 另依赖 `adc.h`(`hadc1`)与 `tpafe5160.h`(快照),主机侧以桩头替身。 +依赖(自下而上、无环):`adc_input.h`(纯枚举 + 声明)← `sample_ring` ← `phasor` ← `freq`; +`prot_set` **独立**;`prot_elm` ← `prot_set` + `phasor` + `freq`。 +`adc_health` 只吃调用方喂进来的计数快照,不反向依赖 L1 实现。 -## 2. 数据流(一拍 = TIM3 1 kHz 节拍) +**除 `adc_input.c` 外全部零平台依赖**(只用 `stdint.h` / `math.h` / `stddef.h` 与本目录头) +→ 可被主机单测 `#include` 原样纳入、免桩跑。`adc_input.c` 另依赖 `adc.h`(`hadc1`)与 +`tpafe5160.h`(快照),主机侧以桩头替身。 -```mermaid -flowchart TD - TIM3["TIM3 更新事件 1 kHz(PSC 83 / ARR 999)
ISR: tpafe5160_start_conv_irq() + adc_tick_irq_notify()
同一事件经 TRGO 触发 ADC1"] --> TPAFE["TPAFE5160 转换
BUSY EXTI 收帧"] - TIM3 --> ADC["STM32 ADC1 一轮 9 通道(≈67 us)
DMA2_Stream4 循环模式自动搬运"] - ADC --> PP["乒乓双缓冲 s_stm32_dma[2×9]
半传输 / 传输完成回调各推进 seq"] - TIM3 -. "ADC_WAKE_FLAG
osThreadFlagsWait" .-> TASK - TPAFE --> POLL - PP --> POLL - TASK["adcTask(事件驱动唤醒,非定时轮询)
App/task/adc_task.c"] --> POLL["adc_input_poll()
TPAFE 快照 + 消费刚完成的那半区(零忙等)"] - POLL --> RAW["adc_input_raw(ch) × 9
按语义取原始码值"] - RAW --> RING["sample_ring_push_frame()
整帧入环(40 点窗)"] - RING --> RUN["phasor_run()
逐实例 20 点重算 + 双区切换"] - RUN --> FREQ["freq_run()
共轭乘积相位增量 + 自适应窗均值(B8)"] - RUN --> GET["phasor_get(idx)"] - FREQ --> FGET["freq_get() / freq_valid()"] - GET --> CONS["消费者:adcTask 日志(现状)
UI / 自检 / L3 保护任务(待接)"] - FGET --> PCONS["消费者:prot_elm_run() OF/UF 判据(B8)
adcTask 心跳 freq 行"] -``` +## 2. 一拍的数据流(一眼版) -初始化落点:`adc_input_init()` 与 `phasor_init()`、`freq_init()` 由 `App/app/app_main.c` 的 -`app_main_init()` 在**内核启动前**调用(各 `*_init()` 均无 RTOS 依赖、幂等); -`phasor_init()` 内部会连带 `sample_ring_init()`,等价于"刚上电"。 +`adc_input_poll()` → `adc_input_raw()` ×9 → `sample_ring_push_frame()` → `phasor_run()` +→ `freq_run()` → 消费者(`adcTask` 心跳与 L1 健康判定;`protectTask` 每 20 ms 取相量 / 频率跑 +`prot_elm_run()`)。节拍 = TIM3 1 kHz 更新事件,两片转换器同源触发。 +完整数据流图与初始化落点 → `docs/算法/采集与算法层设计说明.md` §2。 -## 3. 逐文件说明 +## 3. 逐文件要点(只留"改代码时必须知道"的;机制与理由见设计文档) -### 3.1 `adc_input.c` / `adc_input.h` — L1 ADC 输入层 +> **改本目录前先记四条红线**:① 取数只经 `adc_input_raw()`,不写死下标;② **禁止跨转换器**做 +> 相位 / 功率 / 矢量运算(N-18);③ 标定未落地,现以原始码值充当工程量,**幅值读数不可作精度判据**; +> ④ 本层不建任务、不等事件、不写日志,`*_init()` 才可在内核前调。 +> 全文 → `docs/算法/采集与算法层设计说明.md` §10。 -**解决的问题**:V0.1 上 8 路电气量被两片转换器分片(硬件缺陷 **N-18**),且网名 `AINn` -≠ 板级 `ADCn` ≠ 转换器通道号(13/17 路置换)。若消费侧直接索引驱动缓冲,改版即全线错位。 -本层把"哪一路"收进一张表,上层**按语义取数、不写死下标**。 +### 3.1 `adc_input.c` / `adc_input.h` — L1 语义取数 +- 语义映射表 `k_adc_map[]` 的**内容**唯一权威是 `docs/硬件平台/ADC通道映射说明.md`;V0.2 改版只改表内容,API 不动。 +- `adc_input_poll()` **仅任务上下文**(TPAFE 快照是临界区);STM32 侧固定滞后 1 拍。 +- 四类 STM32 计数(`adc1_seq` / `adc1_gap` / `adc1_err` / `adc1_hw`)成因不同,**勿混读**。 +- 本目录**唯一**允许出现 HAL 调用的文件。→ `docs/算法/采集与算法层设计说明.md` §3。 -| API | 干什么 | 注意 | +### 3.2 `sample_ring.c` / `.h` — L2 输入采样环 +- 整帧写入后才推进写指针 → 取到的窗永远落在整帧边界。 +- 本拍取数失败 → **整拍跳过入环**(`adc_task.c` 的 `skip`),**绝不补 0 点**。 +- 不做跨通道运算、不做丢帧判定(只出 `frames()` 计数)。→ `docs/算法/采集与算法层设计说明.md` §5。 + +### 3.3 `phasor.c` / `.h` — L2 相量计算 +- 系数表预乘 √2/N → `fAmSQ` **直接是有效值**(口径 `docs/算法/傅氏算法详解.md` §2.3)。 +- 实例表 `k_inst[]` 数据驱动:**新增实例只在表尾加一行**。 +- 任一实例不足一周波 → **本拍整体不产出**(不切使用区);每拍 20 点**全部重算**(不做增量递推)。 +- → `docs/算法/采集与算法层设计说明.md` §6。 + +### 3.4 `adc_health.c` / `.h` — L1 有效性判定 +- 三类位**严格分开**:电平(可自愈)/ 锁存(必留痕)/ 边沿事件(仅当拍);**静默 ≠ 丢帧**。 +- **gap 不告警**(消费者超拍是已知现象);告警编排在 `adc_task.c`,不在本层。 +- 首拍 `check()` 只建基线。→ `docs/算法/采集与算法层设计说明.md` §4。 + +### 3.5 `prot_set.c` / `.h` — 保护定值模型(L5-1 载体) +- 装载 **all-or-nothing**:任一项越界整表不装载,越界项由 `prot_set_bad_mask()` 指认。 +- 批量 `prot_set_set()` 后**必须** `prot_set_seal()`;单项原子写用 `prot_set_set_sealed()`。 +- 定值项 / 实例**只许尾部追加**(枚举序 = 数组序 = 校验和覆盖序)。 +- blob **换代(返回 2)≠ 损坏(返回负)**:前者打 INFO、不记自检。→ `docs/算法/采集与算法层设计说明.md` §8、§12.3。 + +### 3.6 `prot_elm.c` / `.h` — L3 保护元件框架 +- 数据缺失两种处理**刻意不同**:相量类任一未就绪 → 整拍跳过;频率类无有效读数 → 按方向填"必不启动"值,**不整拍跳过**。 +- `prot_set_locked()` 或投退 = 0 → 元件整体复位不判据;跨上下文复归走 `prot_elm_request_reset()`。 +- 零序过流(IN 走 STM32)**故意不在实例表**;OF / UF 无独立 104 遥信点,只经 YX15 总聚合可见。 +- `prot_elm_init()` 须在 `phasor_init()` 之后。→ `docs/算法/采集与算法层设计说明.md` §9。 + +### 3.7 `freq.c` / `.h` — 频率测量(L3-5 的测量输入) +- `freq_run()` **仅 adcTask 上下文**,须在 `phasor_run()` 之后同拍调;保护口径 = + `freq_get() == 0 && freq_valid() == 1`。 +- 精度靠**自适应窗长**(窗长 = 泄漏纹波周期的整数倍);定长平均压不到 0.02 Hz, + 换窗长 = 换精度,二者不是独立旋钮。 +- 源切换即废相位基准重建(UA / IA 无共同相位基准,N-18)。 +- 判据 **J1~J6** 与实测口径 → `docs/算法/采集与算法层设计说明.md` §7.2(与 `test/host/freq/freq_test.c` 文件头同源)。 + +## 4. 构建与测试 + +### 4.1 挂载与模拟器边界 +- 七个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`;`../App/algo` 已在 `` 内。 +- 本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路):七个文件全部登记在 + `scripts/check_registration.py` 的 `SIM_EXEMPT`(附理由),`registration-check` 钩子反向校验(N-23)。 + +### 4.2 主机侧单测(L0.5)——生产源文件以 `#include` 原样纳入 + +| 模块 | 命令 | pre-commit 钩子 | |---|---|---| -| `adc_input_init()` | 清缓冲与统计 + **映射表自检**(每个 STM32 语义通道必须落在扫描 Rank 序列内)+ `HAL_ADC_Start_DMA()` 挂载乒乓缓冲(**只挂一次**) | 内核启动前调用;`-1` = 映射指向了从不扫描的通道(会恒读陈旧值);`-2` = DMA 挂载失败 | -| `adc_input_poll()` | 采一拍:`tpafe5160_snapshot()` 原子快照(8 通道)+ **消费刚完成的那半区**(`stm32_take()`,按 `seq` 奇偶选半区,零忙等) | **仅任务上下文**(快照走临界区);只搬码值,不做标定/运算;STM32 侧固定滞后 1 拍 | -| `adc_input_raw(ch, &out)` | 按语义读原始码值(TPAFE 16 位补码 / STM32 0..4095) | 消费侧取数**唯一入口**;须先成功 `poll()`;`-2` = 该来源尚无有效数据 | -| `adc_input_desc(ch)` | 语义 → 物理通道描述(来源 + 下标/通道号) | 供自检注入、UI 通道序;返回静态地址,勿改 | -| `adc_input_stm32_rc()` | STM32 侧最近一拍状态码(0 OK / -2 本拍无新帧) | 心跳日志 `adc1_rc` | -| `adc_input_stm32_seq()` / `_errs()` / `_gaps()` / `_hw_errs()` | DMA 整帧完成计数 / 停摆(连续 ≥2 拍无新帧)/ 丢帧(seq 一次前进 >1)/ HAL 错误回调 | 心跳日志 `adc1_seq` / `adc1_err` / `adc1_gap` / `adc1_hw`——四类成因不同,**勿混读** | +| 相量 / 采样环 | `& '.venv\Scripts\python.exe' test/host/phasor/run_unit_test.py` | `phasor-unit-test` | +| ADC 输入层 | `& '.venv\Scripts\python.exe' test/host/adc_input/run_unit_test.py` | `adc-input-unit-test` | +| 有效性校验 | `& '.venv\Scripts\python.exe' test/host/adc_health/run_unit_test.py` | `adc-health-unit-test` | +| 定值模型 | `& '.venv\Scripts\python.exe' test/host/prot_set/run_unit_test.py` | `prot-set-unit-test` | +| 元件框架 | `& '.venv\Scripts\python.exe' test/host/prot_elm/run_unit_test.py` | `prot-elm-unit-test` | +| 频率测量 | `& '.venv\Scripts\python.exe' test/host/freq/run_unit_test.py` | `freq-unit-test` | -**要点** +改本目录任一 `.c/.h`,提交时对应钩子自动跑。**用例清单与判据数字的属主**是各 +`test/host/<模块>/` 与 `docs/测试说明/` 下的单测说明,本文不复制(三处抄数字必然打架)。 -- 语义映射表 `k_adc_map[]` 的**唯一权威口径**是 `docs/硬件平台/ADC通道映射说明.md`: - `UA/UN/IA` → TPAFE `buf[6]/[5]/[2]`;`UB/UC/IB/IC/IN/TEMP` → ADC1 `CH14/3/6/1/2/5`。 - **V0.2 改版只改这张表的内容**,本层 API 与上层语义不动。 -- STM32 侧是**顺序扫描**(Rank1..8 = CH0..CH7、Rank9 = CH14,与 `MX_ADC1_Init()` 严格一致), - 9 个通道**不是同一时刻**(一轮 ≈67 µs)。v2.0 起这一轮由 **TIM3 TRGO 硬件触发 + DMA 自动搬运**, - 消费侧**零忙等**(旧实现逐通道轮询、单通道超时 2 ms,已于 2026-10-10 移除)。 - 代价:STM32 侧数据**固定滞后 1 拍(1 ms)**——与 TPAFE 侧本来就没有同刻性(N-18),无新增误差。 -- 本文件是本层**唯一**允许出现 HAL 调用的地方(它必须驱动 ADC1);新增模块若不需要外设, - 请保持 `sample_ring` / `phasor` 那种零依赖形态,才能免桩单测。 +### 4.3 板端 +`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍 / 心跳(`phasor(...)`、`freq(...)`、 +`ring` / `skip` / `adc1_*` / `tgap`);健康判据 = **零 `[ERR]`**(健康告警全以 `[ERR]` 开头, +误报会被闭环直接抓住)。→ `docs/算法/采集与算法层设计说明.md` §12。 -### 3.2 `sample_ring.c` / `sample_ring.h` — L2 输入采样环 - -**解决的问题**:相量计算要"以当前时刻为终止的一周波",而 L1 每次只给最新一帧。 -本层按**语义通道**存最近 `SAMPLE_RING_CYCLES = 2` 个周波(`SAMPLE_RING_DEPTH = 40` 点), -与参考系统 `CN_BUFSAM_CYCLE = 2` 一致,并为后续加长数据窗(1.5~2 周波抗衰减直流)预留空间。 - -| API | 干什么 | 返回 | -|---|---|---| -| `sample_ring_init()` | 清全部通道历史与计数(幂等,内核启动前可调) | — | -| `sample_ring_push_frame(values, n)` | 整帧压入(长度须 `= ADC_SEM_COUNT`) | `0` / `-1` 参数错 | -| `sample_ring_window(ch, n, out)` | 取该语义通道最近 `n` 点,**按时间序旧 → 新**(`out[n-1]` 最新) | `0` / `-1` 参数错 / `-2` 点数不足 | -| `sample_ring_filled()` | 已累积点数(**饱和**于 40)——判"够不够一周波" | `0..40` | -| `sample_ring_frames()` | 累计入环帧数(**不饱和**,1 kHz 下约 49.7 天回绕) | 帧数 | - -**要点** - -- **整帧写入后统一推进写指针** → 单帧内不可能"半新半旧",消费侧取到的窗永远落在整帧边界。 -- 本层**只做按时序存/取,不做任何跨通道运算**:同刻性由上游 L1 负责,本层既不保证也不假设。 -- `frames()` 与 L1 的 `tpafe5160_frame_seq()` 对照即可判丢帧(L1-6 的判据之一); - 本层只提供计数,**判定不在这里**。 -- 上层若某通道本拍取数失败,应**整拍跳过入环**(现 `adc_task` 的 `s_frames_skip` 即此), - 切勿补 0 点——0 会污染相量窗。 - -### 3.3 `phasor.c` / `phasor.h` — L2 相量计算 - -**算法**:全周傅氏(单频 DFT),`N = PHASOR_N = 20`(1 kHz @ 50 Hz)。系数表 -`k_fft_sin/k_fft_cos` **离线生成**并**预乘 √2/N**(口径 `docs/算法/傅氏算法详解.md` §2.3), -故 `fAmSQ = √(fRe²+fIm²)` **直接就是有效值**,`fAm = √2·fAmSQ` 为峰值。运行期不调 `sinf/cosf`。 - -| API | 干什么 | 注意 | -|---|---|---| -| `phasor_init()` | 双区清零 + 使用区/就绪复位 + `sample_ring_init()` | 幂等;内核启动前调用 | -| `phasor_inst_count()` / `phasor_inst(idx)` | 遍历实例表(`{name, sem, harm}`) | 返回静态地址,勿改;越界 `NULL` | -| `phasor_run()` | 逐实例取 20 点窗 → 重算 → 写计算区 → **一次性切换使用区** | 仅任务上下文;须先 `push_frame()` | -| `phasor_get(idx, &out)` | 读**使用区**结果 | `0` / `-1` 参数错 / `-2` 未攒满一周波 | -| `phasor_calc(x, n, harm, &out)` | 对给定窗口算 `harm` 次相量 | **纯函数**,可脱离采样环独立单测 | - -**要点** - -- **实例表数据驱动**(`k_inst[]`,现 5 行):`C01-IA-F1`(过流/过负荷)、`C02-UA-F1`(过压/欠压/频率)、 - `C03-UN-F1`(零序过压)、`C04-IA-F2`(二次谐波制动)、`C05-IA-F5`(五次谐波选线)。 - **新增实例只在表尾加一行**,遍历/日志/单测自动适配。 -- **双区(计算区 / 使用区)**:结果先写非使用区,全部实例写完后单字节 `volatile` store 切换 - `s_use_zone`(Cortex-M 上原子)→ 消费侧无锁、无撕裂。今日唯一消费者是 adcTask 自身, - 该机制为后续独立 L3 保护任务预留。 -- **任一实例数据不足一周波 → 本拍整体不产出**(不切换使用区),避免"部分新部分旧"的混合结果。 -- 每拍 20 点**全部重算**(暴力法,无累积误差;迭代加减首尾项会累积)。相量刷新周期 = 1 ms。 -- 相位(L2-4)**未常驻**结构体,需要时自行 `atan2f(fIm, fRe)`。 - -### 3.4 `adc_health.c` / `adc_health.h` — L1-6 采样有效性判定 - -**解决的问题**:驱动侧只有"观测计数"(`tpafe5160_frame_seq/_frstdata_err`、 -`adc_input_stm32_seq/_errs/_gaps/_hw_errs`),**没有判定**——芯片静默、消费者超拍丢帧、 -计数器被改写这些故障此前只能靠人读心跳日志发现。本层把判定收进纯逻辑, -告警编排留在消费者(`adc_task.c` 的 `adc_health_report()`)→ 判定可主机免桩单测。 - -| API | 干什么 | 注意 | -|---|---|---| -| `adc_health_init()` | 清全部状态(幂等) | 首拍 `check()` 只建基线、不判定 | -| `adc_health_check(&in, &out)` | 吃计数快照做一次判定(每拍调,常数次整数运算) | 差值法 `(int32_t)(now-prev)`,tick/计数回绕安全 | -| `adc_health_flags()` | 电平故障位:`TPAFE_STALL` / `STM32_STALL`(≥100 ms 无前进,可自愈)/ `SEQ_BACKWARD`(锁存) | 为 L5-4 闭锁预留 | -| `adc_health_sticky()` | 锁存位(含 `FRSTDATA` / `STM32_ERR` / `HW_ERR` 增量痕迹) | 自愈后仍留痕 | -| `adc_health_tpafe_gaps()` | TPAFE 累计丢帧点数(序号前进 >1 的超出部分) | 与 STM32 侧 `adc_input_stm32_gaps()` 对称 | - -**要点** - -- **三类语义严格分开**:电平位(stall,可自愈)/ 锁存位(出现过必留痕)/ 边沿事件位 - (仅当拍有效)。`ADC_HE_FRSTDATA/STM32_ERR/HW_ERR` 与同名 `ADC_HF_*` **编号对齐**, - 消费者直接用事件掩码对"已留痕位图"做每类一次 `SYSLOG_SELFTEST` 的编排。 -- **gap 不告警**:消费者超拍(心跳日志 ~2 ms)是已知现象,`adc1_gap`/`tgap` 每 10 s 心跳 - 可见;若按事件告警会把真故障淹掉(台架实测开机 `adc1_gap≈24`、稳态每 10 s +2)。 -- **静默 ≠ 丢帧**:d=0 才是静默候选;d>1 只说明消费者没跟上,生产者还活着——两者 - 处置完全不同,判据不许混。 -- 告警策略(在 `adc_task.c`,不在本层):严重事件/电平上升沿 `DBG_ERROR` + - `SYSLOG_SELFTEST`(每类每次开机一次,防刷屏写满 32KB selftest.log);下降沿 `DBG_INFO`。 - -### 3.5 `prot_set.c` / `prot_set.h` — 保护定值模型(P1 / L5-1 载体) - -**解决的问题**:保护判据的"输入宪法"。参考系统把定值放 EEPROM + `tagPRS` 常量表 -校验和自检;本层做 **RAM 模型 + 持久化纯逻辑层**(blob 序列化/解析;文件胶水在 -`App/cfg/prot_set_store.c`,B7),把编号 / 通讯地址 / 范围 / 默认值 / 校验和闭环钉死。 - -| API | 干什么 | 注意 | -|---|---|---| -| `prot_set_init()` | 装默认值 + 封存校验和(幂等) | 内核启动前可调 | -| `prot_set_load_raw(vals)` | 整表装载(B7 由 `prot_set_store_parse` 内部走同一路径),逐项范围校验 | **all-or-nothing**:任一项越界整表不装载,`prot_set_bad_mask()` 指认越界项;影子区 + 单字交换,读侧无中间态 | -| `prot_set_set(id, q10)` | 单项写入(范围校验,批处理路径) | **不自动重封存**——批量改完必须 `prot_set_seal()`,否则 selfcheck 判"非法改写";板端单项写一律用下行 | -| `prot_set_set_sealed(id, q10)` | **原子写+封存(B7)**:影子区改好重封后单字交换 | selfcheck 永不见中间态(收口 B5 竞态窗口);置脏标志 | -| `prot_set_dirty_peek/take/mark()` | 脏标志三件套(B7 落盘握手) | peek=iec104Task 通知;take=属主落盘前消费;mark=写失败回置重试 | -| `prot_set_store_serialize/parse(buf,len)` | 持久化 blob(`PSET`+版本+值区+sum32 正/反码对) | parse **整表原子**:magic/版本/长度/校验对/范围任一失败 = 一字不动 | -| `prot_set_get_f()` / `get_ctl()` | 工程量浮点 / 投退读取 | 单字读原子 | -| `prot_set_selfcheck()` | 重算 sum32 与封存正/反码比对,失配 → 闭锁 | L5-1;seqlock 式复核:提交撞见 = 本轮跳过不误锁;`prot_set_seal()` 是解锁的唯一合法路径 | - -**要点** - -- **Q10 定点**(×1024)与参考 `2^10` 折算口径一致;元数据表(地址/范围/默认)是 - ROM 常量,校验和只覆盖 RAM 值区。 -- **通讯地址逐项对照参考** `Comm/Pub/Const_CommSetPara.c` `g_tCommSetParaTab` - (基址 0x8820);单测 TC-PROTSET-006 保证地址不重复、`min ≤ def ≤ max`、投退项 0..1。 -- ⚠ **默认值/范围为拟定值**(参考 EEPROM 出厂表不在本仓库),待对照修订—— - 见规划 §5.4 落实进度注。 -- **B8 扩 8 项(32→40)**:`PS_OF_EN/OF_HZ/OF_DLY/UF_EN/UF_HZ/UF_DLY/RET_OF/RET_UF` - **尾追加**(编号不回收,枚举序 = 数组序 = 校验和覆盖序)。OF/UF 的逐项通讯地址 - 不在本仓库(参考保护定值段 0x8820~0x8882 仅示"含低压/低频"类目)→ 全部按 - **本地项 comm_addr=0** 登记(104 C_SE 不可寻址),待对照参考表补。 - 频率返回系数**独立**(`RET_OF=0.99 / RET_UF=1.01`,返回门槛 = 定值×系数)—— - 频率滞回带与电气量 0.95/1.05 不同量级,勿共用。 -- **B8 换代语义**:项数变 → blob 144→176 B、`PROT_SET_BLOB_VER` 1→2。B7 旧档 - (ver=1/32 项)在新固件 parse 即被**版本/长度闸**拒绝 → 出厂默认; - `prot_set_store_load()` 返回码分流:**2 = 换代**(`prot_set_store.c` 打 INFO - "prot set blob version change, defaults in use",**不是** CORRUPT、不记自检), - −1 = 真损坏(defaultTask 打 ERROR + 自检留痕)。升级路径不得报成损坏。 -- **bad_mask 随项数扩为 `uint64_t`**(bit i = 第 i 项越界;消费方仅主机单测, - 板端闭锁走 `prot_set_locked()`)。 - -### 3.6 `prot_elm.c` / `prot_elm.h` — 保护元件框架(L3,P1 子集) - -**解决的问题**:参考系统 89 个 `PRS_*.c` 各自实现"滞回 + 定时限"——本框架把这段 -公共核收敛为**一个通用核 + 数据驱动实例表**(同 `phasor` 的 `k_inst` 模式), -新增元件 = 表加一行 + 定值加几项。 - -| API | 干什么 | 注意 | -|---|---|---| -| `prot_elm_init()` | 复位状态 + 解析相量实例索引(按名前缀 `C01/C02/C03`) | 须在 `phasor_init()` 之后调;`src=FREQ` 行无相量绑定(`pidx=0xFF`,run 不查它) | -| `prot_elm_eval(now_ms, meas)` | **纯判据**:定值 RAM + 测量数组 → 全元件状态机 | 主机单测入口;NULL 测量按 0 | -| `prot_elm_run(now_ms)` | 适配层:按实例表 `src` 组测量 → eval(`src=0` 取 `phasor_get()` rms,`src=1` 取 `freq_get()`) | 相量类任一未就绪 → **整拍跳过**(不引入陈旧判据);频率类无有效读数 → **按方向填"必不启动"值**(过量填 0 / 欠量填 `1e30` → 滞回比较恒假 → 元件复位),**不整拍跳过**——频率源缺失不得饿死电流/电压类元件 | -| `prot_elm_state()` / `prot_elm_active_mask()` | 状态快照 / 动作位图 | 供 L4 出口与心跳日志 | - -**要点** - -- **滞回**:已启动用返回门槛(`定值 × 返回系数`),未启动用动作门槛—— - 过量/欠量方向由实例表 `over` 位统一处理(`UV`/`UF` 为欠量实例)。 -- **定时限**:`(uint32_t)(int32_t)(now - t0) >= dly` 差值法,tick 回绕安全; - `dly=0` → 启动同拍即动作。返回**即时**(参考带 10 ms 返回延时,P4 接出口硬件时再补)。 -- **闭锁联动**:`prot_set_locked()`(L5-1)或投退=0 → 该元件状态整体复位、不判据。 -- **V0.1 实例**(N-18):`OC1/OC2/OC3/OL`(IA)、`OV/UV`(UA)、`3U0`(UN 外部通道)、 - `OF/UF`(freq 层测频,B8)。零序过流(IN 走 STM32)**故意不在表中**。 -- **OF/UF 出口可见性(B8)**:两元件**未单独映射 104 遥信点**(送检点表 = B10 口径), - 其动作目前**只经 YX15 保护总聚合可见**(`iec104_pts` 的 fault-total 掩码含 OC/3U0, - OF/UF 动作会置总位但无独立分点)——README 与规划 §3.3 L3-5 行必须同此口径,勿写"独立遥信"。 -- **板端接线已落地(B5,2026-10-12)**:`App/task/protect_task.c`(protectTask, - 20 ms 节拍)消费 `prot_elm_run()` + L5-4 采样故障闭锁 + L5-1 校验和巡检; - 跨上下文复归走 `prot_elm_request_reset()`(104 YK11 落请求、eval 头部消费)。 - 判据结果经 104 遥信上送(`iec104_pts` YX15~23/31)。→ `docs/算法/继电保护功能复现规划.md` §7 - ⚠ 实例数 7→9(B8):protectTask 启动锚点 `elm=` 与 `prot_elm_active_mask()` 位宽随之变, - `.serialtest` 锚点表未含 `elm=` 数字(已核),无需改配置。 - -### 3.7 `freq.c` / `freq.h` — 频率测量层(L2 派生量,L3-5 测量输入,B8) - -**解决的问题**:过频/低频(L3-5)需要系统频率读数,而 L2 只出相量。本层把 -"相邻两拍基波相量的相位差"变成"频率",并解决**非整周波窗长的 DFT 泄漏纹波** -这一决定精度的关键问题。 - -| API | 干什么 | 注意 | -|---|---|---| -| `freq_init()` | 清增量环/计数/读数/相位基准/有效标志(幂等) | 内核启动前可调,须在 `phasor_init()` 之后无要求 | -| `freq_run()` | 每拍步进:选源(UA→IA 过幅值门)→ 共轭乘积 `P=Z(m)·conj(Z(m-1))` 取角 → 假值钳制 → 自适应窗均值 | **仅 adcTask 上下文**,须在 `phasor_run()` 之后同拍调 | -| `freq_get(out)` | 读数(Hz) | 0 有读数(可能是保持值)/ −1 参数错 / −2 从未启动收敛(写 0) | -| `freq_valid()` | "本拍新鲜"标志 | 1 = 幅值门 OK 且已收敛;保护消费口径 = `get()==0 && valid()==1` | -| `freq_last()` | 最近有效读数 | 从未收敛返回 0.0 | - -**算法要点(精度是实测驱动定稿的,两轮弯路记录在 freq.c 文件头)** - -- **相位微分免解缠绕**:`arg(Z_now·conj(Z_prev))` 天然落在 (−π, π],纯音每拍 - 角**增大** 2πf/Fs(本层相量约定 φ=atan2(fIm,fRe)=窗首初相,窗每拍前滑一点); - 45~55 Hz 段 |Δφ| ≈ 0.28~0.35 rad,距 ±π 裕度巨大。 -- **自适应窗长**(核心):20 点矩形窗在非整周波时混入**负序泄漏项** - (45/55 Hz 处 |S₂/S₁| ≈ 3~5%),相量端轨迹成椭圆 → 逐拍相位差带周期 - δ(φ) 纹波,周期 = fs/(2f) ≈ 9.1~11.1 拍。定长窗平均不整除纹波周期 → - 扫频最坏误差 **0.317 Hz**(40 拍算术平均)/ **0.197 Hz**(60 拍复数和 Kay, - 交叉项和角不随 K² 衰减,勿再尝试)。改 `K_eff = round(8·Fs/(2·f̂))` - (45~55 Hz 段 73~89 拍)使窗长恰为纹波周期整数倍 → 纹波相消,最坏 **0.0101 Hz**。 -- **假值钳制带 35~65 Hz**(宽于系统合理带 40.5~60.5 = 参考 `CN_HERTZ_SOFTFR_MIN/MAX`, - 规划 §10.1):单拍相位差本身带 ±5 Hz 纹波,钳制带贴边会**非对称丢弃纹波半周** - → 均值偏置(实测 45 Hz 点 +0.16 Hz,已修)。35~65 仍拦得住窗滑 2 拍的 ≈2f - (90~110 Hz)与相位跳变假值;系统合理带由 OF/UF 定值范围在消费侧把关。 -- **幅值门** `FREQ_AMP_MIN=50.0`(原始码值口径,**拟定待 L1-5 标定复核**): - 低于门 = 无有效波形(浮空/PT 断线 rms≈0,相位无意义)→ 停更新、`valid` 清零、 - 读数保持 → 台架浮空不发散。信号消失过渡段(≤20 ms 残窗)读数漂移有界 ≤0.3 Hz。 -- **源优先级 UA(C02)→IA(C01)**,按名前缀解析、每拍动态回退(台架只注电流亦可测频); - 源切换即废相位基准重建(UA/IA 无共同相位基准,N-18)。 - -**精度判据(J1~J6,与 `test/host/freq/freq_test.c` 文件头同口径,数字为实测)** - -| 判据 | 条件 | 门槛 | 实测最坏 | -|---|---|---|---| -| J1 扫频 | 纯音 45.00~55.00 Hz,0.5 Hz 步进(21 点) | **≤ 0.02 Hz** | **0.0101 Hz** @55.0 Hz | -| J2 噪声 | 幅值 1000 码、噪声 rms = 1% 幅值 | **≤ 0.10 Hz** | **0.0160 Hz** | -| J3 启动收敛 | 首个有效相量 → 首个有效读数 | **≤ 40 ms** | 20 ms(@39 ms 拍,相量 @19 ms) | -| J4 频率阶跃 | 50→51 Hz 进入 ±0.05 Hz 并保持 | **≤ 120 ms** | 98 ms(稳态 err 0.0053 Hz) | -| J5 零幅/浮空 | rms < 幅值门 | valid=0、不发散、保持漂移 ≤0.3 Hz | 漂移 0.13 Hz,无 NaN | -| J6 假值钳制 | 幅值+频率双跳变拍 | 60 ms 内回 ±0.05 Hz | 0.0000 Hz | - -附加鲁棒性(同套件实测):直流偏置 8000 码 → err 0.0018 Hz;3/5 次谐波 20%/10% -→ err 0.0065 Hz;UA 无幅值落 IA → err 0.0020 Hz。 - -**J4 = 120 ms 是"精度优先于响应"的显式取舍(Lead 批准 2026-10-12)**:自适应窗 -(M=8 ≈ 4 个泄漏纹波周期 ≈ 4 个信号周波)换来 0.317→0.0101 Hz,代价是频率骤变后 -的窗冲刷(实测 98 ms)。OF/UF 定时限默认 **1000 ms** → 测频响应占保护总动作时间 -**<10%**;暂态假值另有 J6(跳变后 ≤60 ms 回 ±0.05 Hz)兜底。故 120 ms 是工程整定, -不是判据失守——若将来要压响应,须同时重估精度(缩短窗 = 精度回退到 0.2~0.3 Hz 档)。 - -**板端可见性(DoD)**:adcTask 心跳/首拍日志追加一行 -`freq(hb): f=5000 valid=1 (x0.01)`(MicroLIB 无 %f → ×0.01 定点,分辨率 0.01 Hz)。 -正常带波形应 `valid=1` 且 `f≈5000`;浮空应 `valid=0`、`f` 为保持值。 - -## 4. 关键约定 - -- **语义映射唯一权威口径**:`docs/硬件平台/ADC通道映射说明.md`。取数一律经 `adc_input_raw()`, - 禁止在消费侧直接索引驱动缓冲或写死下标。 -- **禁止跨转换器运算**(V0.1 硬件缺陷 N-18):`UA/UN/IA` 在 TPAFE、`UB/UC/IB/IC/IN` 在 STM32 ADC1, - 两者无共同采样触发 → 无同一时刻性、相位不确定。故本层**不做**跨转换器的相位 / 功率 / - 功率因数 / 负序零序类矢量运算(V0.2 把 8 路电气量全接 TPAFE 后方可解禁)。 -- **标定(L1-5)尚未实现**(缺标准源):现以**原始码值**充当工程量(系数 = 1), - 因此相量读数只证明**链路通**,**不可作幅值精度判据**。 -- **本层不建任务、不等事件、不写日志之外的副作用**;日志由调用侧(adcTask)打。 -- 内核启动前只允许调 `*_init()`;`adc_input_poll()` / `phasor_run()` 仅任务上下文。 - -## 5. 静态足迹(BSS,供栈/RAM 预算参考) - -| 位置 | 量 | -|---|---| -| `sample_ring.c` `s_ring[9][40]` float | **1440 B** | -| `phasor.c` `s_zone[2][5]` `phasor_t` | 160 B | -| `adc_input.c` `s_stm32_raw[16]` + `s_tpafe_raw[8]` | 48 B | -| `adc_health.c` 内部状态(9 × uint32 + uint8) | ≈ 40 B | -| `prot_set.c` `s_val[2][40]` + 封存/位图(64 位)/闭锁 | 336 B(B8:31→40 项) | -| `prot_elm.c` `s_rt[9]`(16 B/实例) | 144 B(B8:7→9 实例) | -| `freq.c` 相位增量环 `fq_d[96]` + 占位标志 + 状态 | **≈ 520 B**(B8 新增) | -| 运行期栈:`phasor_run()` 的 `win[20]` | 80 B/帧 | -| 运行期栈:`freq_run()` 局部 | ≈ 30 B/帧(无大局部数组) | - -改容量(`SAMPLE_RING_CYCLES` / 实例数 / `ADC_SEM_COUNT` / `FREQ_AVG_TAPS`)前后各跑一次 -`& '.venv\Scripts\python.exe' scripts/stack_report.py`(口径见 `mem-budget` skill)。 -**B8 实测(adcTask 静态最坏链)**:见 READY 消息与 `docs/内存管理/` 台账回填。 - -## 6. 构建与测试 - -- **挂载**:八个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`,`../App/algo` 已在 `` 内。 - 本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路)——纯算法且模拟器无消费路径的 - 新文件(`prot_set.c` / `freq.c`)改在 `scripts/check_registration.py` 的 `SIM_EXEMPT` 登记豁免 - (`registration-check` 钩子反向校验,N-23)。 -- **主机侧单测(L0.5)**——生产源文件以 `#include` 原样纳入,无需改产品代码: - - | 套件 | 命令 | pre-commit 钩子 | - |---|---|---| - | 相量层(57 用例:系数表逐项复核 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 双区交换) | `& '.venv\Scripts\python.exe' test/host/phasor/run_unit_test.py` | `phasor-unit-test` | - | ADC 输入层(逐 Rank 注入互异码值钉死映射 + HAL 启动失败/超时分支) | `& '.venv\Scripts\python.exe' test/host/adc_input/run_unit_test.py` | `adc-input-unit-test` | - | 有效性校验(31 用例:100 ms 静默门限两侧 / 丢帧边沿 / 倒退锁存 / 错误增量 / tick 回绕 / NULL 守卫 / init 幂等) | `& '.venv\Scripts\python.exe' test/host/adc_health/run_unit_test.py` | `adc-health-unit-test` | - | 定值模型(90 用例:范围校验 / all-or-nothing 装载 / 封存-自检-闭锁 / Q10 往返 / 元数据表自检 / B7 blob 往返与损坏拒绝 / **B8 OF-UF 行 + 64 位 bad_mask + 换代拒绝**) | `& '.venv\Scripts\python.exe' test/host/prot_set/run_unit_test.py` | `prot-set-unit-test` | - | 元件框架(47 用例:滞回与定时限 / 欠量方向 / 投退 / 闭锁联动 / 时基回绕 / 位图聚合 / **B8 OF-UF 滞回与方向隔离 + run() 测频接线**) | `& '.venv\Scripts\python.exe' test/host/prot_elm/run_unit_test.py` | `prot-elm-unit-test` | - | **频率测量(46 用例:J1 扫频 45~55 Hz / J2 噪声 / J3 启动收敛 / J4 阶跃 / J5 浮空不发散 / J6 假值钳制 / 直流·谐波抑制 / UA→IA 回退 / 守卫与幂等)** | `& '.venv\Scripts\python.exe' test/host/freq/run_unit_test.py` | `freq-unit-test`(**待登记**,合并时由 Lead 加钩子) | - - 改本目录任一 `.c/.h`,提交时对应钩子自动跑;详见 `docs/测试说明/相量层单元测试说明.md`。 -- **板端**:`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍/心跳日志(`phasor(...)` 行、 - `freq(...)` 行、`ring` / `skip` 计数、`adc1_rc` / `adc1_err`、`tgap`);健康判据 = 零 `[ERR]` - (`adc_health_report` 的告警全部以 `[ERR]` 开头 → 误报会被闭环直接抓住)。 -- **定值档升级 vs 损坏(B8,台架判读)**:`prot_set_store_load()` **rc=2 = 换代** - (旧固件/旧项数的档被版本闸拒绝)→ 只打 INFO `prot set blob version change, defaults in use`, - **不是** `[ERR] ... CORRUPT`、不记自检;**rc<0 = 真损坏** → `[ERR] prot set blob CORRUPT` + 自检留痕。 - 升级固件后首启看到"version change"一行属**预期**,勿当故障排查。 - -## 7. 新增一个算法模块的固定动作 +## 5. 新增一个算法模块的固定动作 1. 在本目录建 `xxx.c/h`,文件头写模块注释块(规范 V1.0),**尽量零平台依赖**以便免桩单测。 -2. 用 `new-source-file` skill 注册到 `MDK-ARM/STM32F407-Demo.uvprojx`(目录已在 IncludePath, - 若新建子目录则必须补 IncludePath)。模拟器侧二选一:有消费路径 → 加 - `simulator/CMakeLists.txt`;**纯算法无消费路径 → 加 `scripts/check_registration.py` - 的 `SIM_EXEMPT`**(照 `prot_set.c` / `freq.c` 先例写理由),否则 `registration-check` 钩子拦。 +2. 用 `new-source-file` skill 注册到 `MDK-ARM/STM32F407-Demo.uvprojx`(新建子目录还要补 ``)。 + 模拟器侧二选一:有消费路径 → 加 `simulator/CMakeLists.txt`;纯算法无消费路径 → 加 + `scripts/check_registration.py` 的 `SIM_EXEMPT`(照本目录既有七条写理由),否则 `registration-check` 拦。 3. 加 `test/host/<模块>/` 主机单测 + `.pre-commit-config.yaml` 钩子(照 `phasor-unit-test` 抄)。 -4. 消费者写在 `App/task/`,本层只出"数";跨转换器运算的禁令见 §4。 +4. 消费者写在 `App/task/`,本层只出"数";跨转换器禁令见 §3 开头四条红线。 +5. 在本 README §1 清单加一行,并在 `docs/算法/采集与算法层设计说明.md` 里写清该模块的契约与理由。 -## 8. 相关文档 +## 6. 相关文档 +- **本层设计与判据**:`docs/算法/采集与算法层设计说明.md` - 通道映射权威口径:`docs/硬件平台/ADC通道映射说明.md` -- 分层与路线图(L1/L2 清单与状态):`docs/算法/继电保护功能复现规划.md` +- 分层与功能项清单:`docs/算法/继电保护功能复现规划.md` - 傅氏算法与有效值口径:`docs/算法/傅氏算法详解.md` -- 采样节拍(TIM3 1 kHz + TRGO 触发 ADC1/DMA):`docs/子系统设计/ADC采样节拍设计说明.md` -- 相量层单测细则:`docs/测试说明/相量层单元测试说明.md` +- 采样节拍(TIM3 1 kHz + TRGO 触发 ADC1 / DMA):`docs/子系统设计/ADC采样节拍设计说明.md` +- 相量层 / ADC 输入层单测细则:`docs/测试说明/相量层单元测试说明.md`、`docs/测试说明/ADC输入层单元测试说明.md` +- RAM 与栈台账:`docs/内存管理/RAM使用分析与优化方案.md` diff --git a/App/algo/freq.h b/App/algo/freq.h index 1fe8860f..fcbcb3ba 100644 --- a/App/algo/freq.h +++ b/App/algo/freq.h @@ -9,7 +9,7 @@ * 推导与实测见 freq.c 文件头与 test/host/freq 输出)。 * 覆盖规划 §3.3 L3-5 的测量侧与 §4.2 测频范围(45~55 Hz 建议段)。 * - * 原理与判据(详见 App/algo/README.md §3.7 与 test/host/freq/): + * 原理与判据(详见 docs/算法/采集与算法层设计说明.md §7 与 test/host/freq/): * - 采样率 Fs = 1000 Hz(TIM3 节拍,与 phasor 同源);45~55 Hz 段每拍 * 相位增量 ≈ 0.28~0.35 rad,距 ±π 缠绕边界裕度巨大,无歧义; * - 瞬时值钳制:单拍相位差折算越出 35~65 Hz(**假值带**,宽于系统合理带 diff --git a/docs/README.md b/docs/README.md index f8c0c821..68e70532 100644 --- a/docs/README.md +++ b/docs/README.md @@ -104,6 +104,7 @@ docs/ | 文档 | 内容 | |---|---| | [`傅氏算法详解.md`](算法/傅氏算法详解.md) | 全周波傅氏(单频 DFT)原理与工程实现:正交性、梳状带通、滑动窗、系数表折算有效值;保护/遥测双轨有效值的算法基础 | +| [`采集与算法层设计说明.md`](算法/采集与算法层设计说明.md) | **`App/algo/` 的现状设计说明**:L1 语义取数与有效性判定、L2 采样环/相量/测频、L3 定值模型与元件框架的契约、关键机制(双区原子切换、乒乓半区、自适应测频窗、定值封存-自检-闭锁)、精度判据 J1~J6 与资源足迹 | | [`继电保护功能复现规划.md`](算法/继电保护功能复现规划.md) | **保护功能复现规划**:把参考系统(CLS100)保护链路映射到本项目(STM32F4)的分层功能/性能/参数清单、平台适配(N-18 等)与分阶段路线图 | --- diff --git a/docs/代码规范/pre-commit使用说明.md b/docs/代码规范/pre-commit使用说明.md index 86a64d58..4482f0cc 100644 --- a/docs/代码规范/pre-commit使用说明.md +++ b/docs/代码规范/pre-commit使用说明.md @@ -406,7 +406,7 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法 | **触发条件** | `App/algo/freq.*`、`App/algo/phasor.*`、`App/algo/sample_ring.*`、`test/host/freq/**` 或 `test/host/common/unit_test_harness.py` 变动 | | **处理方式** | **阻止提交**(单测失败即拦截) | | **被测对象** | `App/algo/freq.c/h`(L2.5 测频:共轭乘积相位增量 + 解缠绕 + 自适应窗长 K_eff 对齐负序泄漏纹波周期 + 假值钳制带 35~65 Hz + 幅值门 valid=0)——纯合成信号、零 I/O | -| **用例** | 46 断言:J1 扫频 45~55 Hz ≤0.02 Hz(实测 0.0101)/ J2 噪声 1% ≤0.10 Hz / J3 启动收敛 ≤40 ms / J4 阶跃 50→51 ≤120 ms(精度优先于响应,见 `App/algo/README.md` §3.7)/ J5 浮空 valid=0 不发散 / J6 双跳变钳制恢复 | +| **用例** | 46 断言:J1 扫频 45~55 Hz ≤0.02 Hz(实测 0.0101)/ J2 噪声 1% ≤0.10 Hz / J3 启动收敛 ≤40 ms / J4 阶跃 50→51 ≤120 ms(精度优先于响应,见 `docs/算法/采集与算法层设计说明.md` §7.2)/ J5 浮空 valid=0 不发散 / J6 双跳变钳制恢复 | | **为什么需要** | 测频精度是 OF/UF 判据的地基;板端无标准源照不出来(规划 §3.3 L3-5 注入验收归 B10);定长窗负序泄漏纹波(0.317 Hz)这类坑只有扫频合成信号才能暴露 | | **运行器** | `test/host/freq/run_unit_test.py`(MSVC `cl.exe`,L0.5) | diff --git a/docs/子系统设计/ADC采样节拍设计说明.md b/docs/子系统设计/ADC采样节拍设计说明.md index 4de35827..ccd0f2b3 100644 --- a/docs/子系统设计/ADC采样节拍设计说明.md +++ b/docs/子系统设计/ADC采样节拍设计说明.md @@ -5,7 +5,7 @@ > **文档定位**:**现状设计说明**——这条链当前由谁产生节拍、同一个硬件事件如何驱动两片转换器、 > STM32 侧怎么搬运、消费者怎么取数、为什么这样配置、边界与代价在哪里。**历史演进、方案对比、 > 门禁与台架复测记录、修订日志一律不写进本文**(需要时查 git 历史)。 -> **状态**:已落地并经真机验证;下游 = L2 相量 / 测频 → L3 保护判据(`App/algo/README.md`)。 +> **状态**:已落地并经真机验证;下游 = L2 相量 / 测频 → L3 保护判据(`docs/算法/采集与算法层设计说明.md`)。 > **相关文档**: > `docs/硬件平台/ADC通道映射说明.md`(网名 ↔ 板级 `ADCn` ↔ 转换器通道号,唯一权威) > `docs/算法/继电保护功能复现规划.md` §3.1 L1-1(上位需求:1000 Hz 周期 CONVST 重触发) @@ -88,7 +88,7 @@ flowchart TD | `App/algo/adc_input.c` | L1:`adc_input_poll()` / `adc_input_raw()`、半区推导、DMA 完成回调、四类故障计数 | 手写 | | `App/task/adc_task.c` / `adc_task.h` | 消费者主循环、`adc_tick_irq_notify()` 与就绪门、10 s 心跳统计 | 手写 | | `Src/freertos.c` | `adcTask` 栈 640 words、优先级 `osPriorityAboveNormal` | 手写于 `USER CODE` 区(不在 `.ioc`);改栈前后各跑一次 `stack_report.py` | -| `App/task/protect_task.c` | 下游消费者:20 ms 判据节拍取 L2 相量快照 | → `App/algo/README.md` | +| `App/task/protect_task.c` | 下游消费者:20 ms 判据节拍取 L2 相量快照 | → `docs/算法/采集与算法层设计说明.md` §9 | --- ## 3. 节拍源:TIM3 配置与启动 diff --git a/docs/架构设计/项目架构总览.md b/docs/架构设计/项目架构总览.md index c937978d..bd4eeb6e 100644 --- a/docs/架构设计/项目架构总览.md +++ b/docs/架构设计/项目架构总览.md @@ -329,7 +329,8 @@ flowchart LR | 存储架构(NAND/FTL/FatFs) | `docs/架构设计/STM32F4-Base存储架构说明.md` | | NET 阻塞模型(挂起请求表五路语义) | `docs/网络/Net层阻塞模型架构设计.md` | | 网络自愈(监听重建 / 沉降隔离 / 周期对账) | `docs/网络/网络自愈架构设计.md` | -| 采集与算法层 L1~L3 | `App/algo/README.md` | +| 采集与算法层 L1~L3(**设计与判据**) | `docs/算法/采集与算法层设计说明.md` | +| 采集与算法层目录索引(文件清单 / 构建测试) | `App/algo/README.md` | | ADC 采样节拍(TIM3 1kHz 同源驱动) | `docs/子系统设计/ADC采样节拍设计说明.md` | | IEC 60870-5-104 上行(:2404) | `docs/子系统设计/IEC104上行设计说明.md` | | 墙钟时间维护 | `docs/子系统设计/系统墙钟时间维护说明.md` | diff --git a/docs/算法/继电保护功能复现规划.md b/docs/算法/继电保护功能复现规划.md index 3f4dff4b..f80d04b7 100644 --- a/docs/算法/继电保护功能复现规划.md +++ b/docs/算法/继电保护功能复现规划.md @@ -257,7 +257,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-4 | 相位输出 | `φ = atan2(fIm, fRe)` | 相位误差 ≤ 目标值 | ✅ 2026-10-11(链路级):`fRe/fIm` 常驻双区、每拍产出,φ 由消费侧 `atan2f` 现算(不常驻结构体,见 `docs/算法/采集与算法层设计说明.md` §6);单测覆盖相位;**目标值待产品定义 + 标准源复核** | | 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 任务预留无锁读 | diff --git a/docs/算法/采集与算法层设计说明.md b/docs/算法/采集与算法层设计说明.md new file mode 100644 index 00000000..03c02522 --- /dev/null +++ b/docs/算法/采集与算法层设计说明.md @@ -0,0 +1,413 @@ +# 采集与算法层设计说明(`App/algo/`:L1 → L2 → L3) + +> **模块名称**:自研采集与算法层,代码在 `App/algo/`(七个 `.c`,全部登记进 Keil 工程)。 +> **文档定位**:**现状设计说明**——本层当前如何组织、每个模块的契约与关键机制、为什么这样设计、 +> 边界与代价。**目录级索引(文件清单、构建与测试命令、新增模块动作)在 `App/algo/README.md`**, +> 本文不重复其内容;本文不写历史演进、门禁复测记录与修订日志(需要时查 git 历史)。 +> **状态**:L1 → L2 → L3 主线已闭环到板端消费者(`protectTask`,20 ms 判据节拍); +> 标定(`docs/算法/继电保护功能复现规划.md` §3.1 L1-5)尚未实现,幅值精度不在本层能力内。 +> **相关文档**: +> 分层与功能项(L1~L5)的唯一属主登记处:`docs/算法/继电保护功能复现规划.md` §2.1(分层)、§3(功能清单)、§4.2(精度)、§7(路线图) +> `docs/硬件平台/ADC通道映射说明.md`(**语义映射唯一权威口径**) +> `docs/子系统设计/ADC采样节拍设计说明.md`(TIM3 1 kHz 同源节拍、ADC1+DMA 乒乓缓冲、`adcTask` 唤醒) +> `docs/算法/傅氏算法详解.md` §2.3(有效值口径怎么来的) +> `docs/测试说明/相量层单元测试说明.md`、`docs/测试说明/ADC输入层单元测试说明.md`(主机单测细则) + +## 目录 + +- [1. 定位与边界](#1-定位与边界) +- [2. 一拍的数据流与初始化落点](#2-一拍的数据流与初始化落点) +- [3. L1 语义取数 `adc_input.c`](#3-l1-语义取数-adcinputc) +- [4. L1 有效性判定 `adc_health.c`](#4-l1-有效性判定-adchealthc) +- [5. L2 输入 `sample_ring.c`](#5-l2-输入-sampleringc) +- [6. L2 `phasor.c` 相量计算](#6-l2-phasorc-相量计算) +- [7. L2 派生量:频率测量 `freq.c`](#7-l2-派生量频率测量-freqc) +- [8. L3 输入:保护定值模型 `prot_set.c`](#8-l3-输入保护定值模型-protsetc) +- [9. L3 保护元件框架 `prot_elm.c`](#9-l3-保护元件框架-protelmc) +- [10. 关键约定与禁令](#10-关键约定与禁令) +- [11. 资源足迹(改容量前先看这里)](#11-资源足迹改容量前先看这里) +- [12. 验证与判读](#12-验证与判读) +- [13. 相关文档](#13-相关文档) + +--- + +## 1. 定位与边界 + +- **原则:算法与 I/O、与调度分离。** 本层只做两件事——把原始码值变成**语义通道**的"数"、把采样序列 + 变成**相量/频率/判据状态**。不建任务、不等事件、不碰寄存器(唯一例外 `adc_input.c`,它必须驱动 + ADC1)、不做出口动作。 +- **这样分离换到什么**:`sample_ring` / `phasor` / `adc_health` / `prot_set` / `prot_elm` / + `freq` **零平台依赖**(只用 `stdint.h` / `math.h` / `stddef.h` 与本目录头),因此可被主机侧 + 单测以 `#include` 原样纳入、离线全覆盖(落实登记簿 **A-4**),不需要真机就能钉死判据。 +- **上下文约束**:`*_init()` 一律无 RTOS 依赖、幂等,由 `app_main_init()` 在**内核启动前**调用; + `adc_input_poll()` / `phasor_run()` / `freq_run()` **仅任务上下文**(快照走临界区)。 +- **消费者在 `App/task/`**:`adcTask` 采集并压环、`protectTask`(B5)跑 `prot_elm_run()`; + 新算法一律落在 `App/algo/`,**勿塞进 `App/task/`**(任务只做消费与编排)。 + +--- + +## 2. 一拍的数据流与初始化落点 + +一拍 = TIM3 的一个 1 kHz 更新事件(节拍与消费者唤醒同源,见 +`docs/子系统设计/ADC采样节拍设计说明.md` §1)。 + +```mermaid +flowchart TD + TIM3["TIM3 更新事件 1 kHz
ISR: 脉冲 CONVST + 置唤醒标志
同一事件经 TRGO 触发 ADC1"] + TIM3 --> TPAFE["TPAFE5160 转换
BUSY EXTI 收帧"] + TIM3 --> ADC["STM32 ADC1 一轮 9 通道
DMA2 循环模式自动搬运"] + ADC --> PP["乒乓双缓冲
半传输 / 整传输回调推进完成计数"] + TIM3 -. "唤醒 adcTask" .-> TASK + TPAFE --> POLL + PP --> POLL + TASK["adcTask(事件驱动唤醒,非定时轮询)"] --> POLL["adc_input_poll()
TPAFE 原子快照 + 消费刚完成的半区"] + POLL --> RAW["adc_input_raw(ch) × 9
按语义取原始码值"] + RAW --> RING["sample_ring_push_frame()
整帧入环"] + RING --> RUN["phasor_run()
逐实例重算 + 双区切换"] + RUN --> FREQ["freq_run()
共轭乘积相位增量 + 自适应窗均值"] + RUN --> GET["phasor_get(idx)"] + FREQ --> FGET["freq_get() / freq_valid()"] + GET --> CONS["消费者:adcTask 心跳日志"] + FGET --> PCONS["消费者:prot_elm_run() 的 OF / UF 判据(B8)"] +``` + +| 落点 | 做什么 | +|---|---| +| `app_main_init()`(内核启动前) | `adc_input_init()`(映射自检 + 挂 DMA 缓冲)→ `phasor_init()`(内部连带 `sample_ring_init()`)→ `freq_init()` → `prot_set_init()` → `prot_elm_init()` | +| `adcTask`(每拍) | `adc_input_poll()` → `adc_input_raw()` ×9 → `sample_ring_push_frame()` → `phasor_run()` → `freq_run()` → 心跳与健康判定 `adc_health_check()` | +| `protectTask`(20 ms) | 取 `phasor_get()` rms 与 `freq_get()` → `prot_elm_run()`;L5-4 采样故障闭锁、L5-1 定值校验和巡检 | + +`prot_elm_init()` 须在 `phasor_init()` **之后**(它按名前缀解析相量实例索引);`prot_set` 独立 +(只依赖 `stdint.h`),可在任意点初始化。 + +--- + +## 3. L1 语义取数 `adc_input.c` + +**解决的问题**:V0.1 上 8 路电气量被两片转换器分片(硬件缺陷 **N-18**),且网名 `AINn` +≠ 板级 `ADCn` ≠ 转换器通道号(17 路里 13 路置换)。消费侧若直接索引驱动缓冲,改版即全线错位。 +本层把"哪一路"收进一张表 `k_adc_map[]`,上层**按语义取数、不写死下标**。 + +| API | 契约 | 违约后果 | +|---|---|---| +| `adc_input_init()` | 清缓冲与统计 + **映射自检**(每个 STM32 语义通道必须落在扫描 Rank 序列内)+ `HAL_ADC_Start_DMA()` 挂载乒乓缓冲(**只挂一次**);`-1` = 映射指向从不扫描的通道,`-2` = DMA 挂载失败 | 映射错位是**静默**的:该路恒读上一个陈旧值,不报错 | +| `adc_input_poll()` | 采一拍:`tpafe5160_snapshot()` 原子快照(8 通道)+ `stm32_take()` 消费**刚完成的那半区**(按完成计数奇偶推半区,零忙等) | **仅任务上下文**(快照是临界区);在 ISR 里调会破坏 `tpafe5160_snapshot()` 的"仅任务上下文"约定 | +| `adc_input_raw(ch, &out)` | 按语义读原始码值(TPAFE 16 位补码 / STM32 `0..4095`);`-2` = 该来源尚无有效数据 | 消费侧取数**唯一入口**;须先成功 `poll()` | +| `adc_input_desc(ch)` | 语义 → 物理通道描述(来源 + 下标 / 通道号),供自检注入与 UI 通道序 | 返回静态地址,**勿改** | +| `adc_input_stm32_rc()` / `_seq()` / `_errs()` / `_gaps()` / `_hw_errs()` | 最近一拍状态码 / DMA 整帧完成计数 / 停摆(连续 ≥2 拍无新帧)/ 丢帧(计数一次前进 >1)/ HAL 错误回调 | **四类成因不同,勿混读**:对应心跳字段 `adc1_rc` / `adc1_seq` / `adc1_err` / `adc1_gap` / `adc1_hw`(判读见 `docs/子系统设计/ADC采样节拍设计说明.md` §8) | + +**要点** + +- **映射表内容的唯一权威口径**是 `docs/硬件平台/ADC通道映射说明.md`:`UA/UN/IA` → TPAFE + `buf[6]/[5]/[2]`;`UB/UC/IB/IC/IN/TEMP` → ADC1 `CH14/3/6/1/2/5`。 + **V0.2 改版只改这张表的内容**,本层 API 与上层语义不动。 +- **STM32 侧是顺序扫描**(Rank1..8 = CH0..CH7、Rank9 = CH14,与 `MX_ADC1_Init()` 严格一致), + 9 个通道**不是同一时刻**(一轮 ≈67 µs);由 TIM3 TRGO 硬件触发 + DMA 自动搬运( + `docs/子系统设计/ADC采样节拍设计说明.md` §4)。STM32 侧数据**固定滞后 1 拍(1 ms)**——与 TPAFE + 侧本来就没有同刻性(N-18),无新增误差。 +- **开机映射自检**挡住"映射到一个从不扫描的通道"这类静默错(启动日志 `ADC input layer init: OK`)。 +- 本文件是本层**唯一**允许出现 HAL 调用的地方;新增模块若不需要外设,请保持 `sample_ring` / + `phasor` 那种零依赖形态,才能免桩单测。 + +--- +## 4. L1 有效性判定 `adc_health.c` + +**解决的问题**:驱动侧与 L1 只有"观测计数"(`tpafe5160_frame_seq()` / +`tpafe5160_frstdata_err()`、`adc_input_stm32_*()`),**没有判定**——芯片静默、消费者超拍丢帧、 +序号倒退这些故障原本只能靠人读心跳日志发现。本层把判定收进纯逻辑,**告警编排留在消费者** +(`adc_task.c` 的 `adc_health_report()`)→ 判定可主机免桩单测。 + +| API | 契约 | +|---|---| +| `adc_health_init()` | 清状态;**首拍 `check()` 只建基线、不判定** | +| `adc_health_check(&in, &out)` | 吃"计数快照"做一次判定(每拍调,常数次整数运算);差值法 `(int32_t)(now - prev)` → 计数与 tick 回绕都安全 | +| `adc_health_flags()` | **电平**故障位:`TPAFE_STALL` / `STM32_STALL`(≥100 ms 无前进,可自愈)/ `SEQ_BACKWARD`(锁存)——为 L5-4 闭锁供数 | +| `adc_health_sticky()` | **锁存**位:`FRSTDATA` / `STM32_ERR` / `HW_ERR` 的增量痕迹,自愈后仍留痕 | +| `adc_health_tpafe_gaps()` | TPAFE 累计丢帧点数(序号前进 >1 的超出部分),与 STM32 侧 `adc_input_stm32_gaps()` 对称(心跳字段 `tgap`) | + +**要点** + +- **三类语义严格分开**:**电平**位(stall,可自愈)/ **锁存**位(出现过必留痕)/ **边沿事件**位 + (仅当拍有效)。`ADC_HE_*` 与同名 `ADC_HF_*` **编号对齐**,消费者直接用事件掩码对"已留痕位图" + 做"每类每次开机一次 `SYSLOG_SELFTEST`"的编排。 +- **静默 ≠ 丢帧**:前进量 d = 0 才是静默候选;d > 1 只说明消费者没跟上、生产者还活着—— + 两者处置完全不同,判据不许混。 +- **gap 不告警**:消费者超拍(心跳拍 ≈2 ms)是已知现象,`adc1_gap` / `tgap` 在 10 s 心跳里可见; + 若按事件告警会把真故障淹掉。 +- 告警策略(在 `adc_task.c`,**不在本层**):严重事件与电平上升沿 `DBG_ERROR` + + `SYSLOG_SELFTEST`(每类每次开机一次,防刷屏写满 `selftest.log`);下降沿 `DBG_INFO` + ("恢复"必须可见,否则只看到一次错误无从判断是否还在)。 + +--- + +## 5. L2 输入 `sample_ring.c` + +**解决的问题**:相量计算要"以当前时刻为终止的一周波",而 L1 每次只给最新一帧。本层按**语义通道** +存最近 `SAMPLE_RING_CYCLES = 2` 个周波(`SAMPLE_RING_DEPTH = 40` 点),与参考系统 +`CN_BUFSAM_CYCLE = 2` 一致,并为后续加长数据窗(1.5~2 周波抗衰减直流)预留空间。 + +| API | 契约 | +|---|---| +| `sample_ring_push_frame(values, n)` | 整帧压入,长度必须 `= ADC_SEM_COUNT` | +| `sample_ring_window(ch, n, out)` | 取该语义通道最近 `n` 点,**按时间序旧 → 新**(`out[n-1]` 最新);`-2` = 点数不足 | +| `sample_ring_filled()` | 已累积点数(**饱和**于 40)——"够不够一周波"的判据 | +| `sample_ring_frames()` | 累计入环帧数(**不饱和**,1 kHz 下约 49.7 天回绕)——与 `tpafe5160_frame_seq()` 对照即判丢帧 | + +**要点** + +- **整帧写入后才统一推进写指针** → 单帧内不可能"半新半旧",消费侧取到的窗永远落在整帧边界。 +- 本层**只做按时序存 / 取,不做任何跨通道运算**:同刻性由上游 L1 负责,本层既不保证也不假设。 + 判定也不在本层(本层只出计数)。 +- 某通道本拍取数失败 → **整拍跳过入环**(现 `adc_task.c` 的 `s_frames_skip` 即此), + **切勿补 0 点**——单个 0 点会显著污染 20 点 DFT 窗。 + +--- + +## 6. L2 `phasor.c` 相量计算 + +**算法**:全周傅氏(单频 DFT),`N = PHASOR_N = 20`(1 kHz @ 50 Hz)。系数表 +`k_fft_sin` / `k_fft_cos` **离线生成**并**预乘 √2/N**(口径推导见 +`docs/算法/傅氏算法详解.md` §2.3),故 `fAmSQ = √(fRe²+fIm²)` **直接就是有效值**, +`fAm = √2·fAmSQ` 为峰值。运行期不调 `sinf/cosf`(省算力,也让结果可逐点复算验证)。 + +| API | 契约 | +|---|---| +| `phasor_init()` | 双区清零 + 使用区/就绪复位 + `sample_ring_init()`(等价"刚上电");幂等、内核前可调 | +| `phasor_inst_count()` / `phasor_inst(idx)` | 遍历实例表 `{name, sem, harm}`;返回静态地址**勿改**,越界 `NULL` | +| `phasor_run()` | 逐实例取 20 点窗 → 重算 → 写计算区 → **全部实例写完后一次性切换使用区**;仅任务上下文,须先 `push_frame()` | +| `phasor_get(idx, &out)` | 读**使用区**结果;`-2` = 未攒满一周波 | +| `phasor_calc(x, n, harm, &out)` | 对给定窗口算 `harm` 次相量——**纯函数**,可脱离采样环独立单测 | + +**要点** + +- **实例表数据驱动**(`k_inst[]`,现 5 行):`C01-IA-F1`(过流 / 过负荷)、`C02-UA-F1` + (过压 / 欠压 / 频率)、`C03-UN-F1`(零序过压)、`C04-IA-F2`(二次谐波制动)、 + `C05-IA-F5`(五次谐波选线)。**新增实例只在表尾加一行**,遍历 / 日志 / 单测自动适配。 +- **双区(计算区 / 使用区)**:结果先写非使用区,全部实例写完后单字节 `volatile` store 切换 + `s_use_zone`(Cortex-M 上原子)→ 消费侧无锁、无撕裂。当前唯一消费者是 `adcTask` 自身, + 该机制为 `protectTask` 之外的第二个并发读者预留。 +- **任一实例数据不足一周波 → 本拍整体不产出**(不切换使用区):宁可整体不刷新,也不给出 + "部分新部分旧"的混合结果。 +- **每拍 20 点全部重算**(暴力法)。不用"加尾项去头项"的增量式:增量递推会累积误差, + 暴力法每次都是干净的一周波。代价 = 每拍 20 × 实例数次乘加,相量刷新周期 1 ms。 +- **相位(L2-4)不常驻结构体**:需要时自行 `atan2f(fIm, fRe)`(`φ` 约定 = 窗首初相,见 §7)。 + +--- +## 7. L2 派生量:频率测量 `freq.c` + +**解决的问题**:过频 / 低频(`docs/算法/继电保护功能复现规划.md` §3.3 L3-5)需要系统频率读数, +而 L2 只出相量。本层把"相邻两拍基波相量的相位差"变成"频率",并解决**非整周波窗长的 DFT 泄漏纹波** +——这是决定精度的关键问题。 + +| API | 契约 | +|---|---| +| `freq_init()` | 清增量环 / 计数 / 读数 / 相位基准 / 有效标志;幂等,内核前可调 | +| `freq_run()` | 每拍步进:选源(UA → IA 过幅值门)→ 共轭乘积 `P = Z(m)·conj(Z(m-1))` 取角 → 假值钳制 → 自适应窗均值。**仅 `adcTask` 上下文,须在 `phasor_run()` 之后同拍调** | +| `freq_get(out)` | `0` 有读数(可能是保持值)/ `-1` 参数错 / `-2` 从未收敛(写 0) | +| `freq_valid()` | "本拍新鲜":幅值门 OK 且已收敛。**保护消费口径 = `freq_get() == 0 && freq_valid() == 1`** | +| `freq_last()` | 最近有效读数;从未收敛返回 `0.0` | + +### 7.1 为什么是"相位微分 + 自适应窗" + +- **免解缠绕**:`arg(Z_now · conj(Z_prev))` 天然落在 `(-π, π]`,无需维护累积相位。 + 本层相量约定 `φ = atan2(fIm, fRe)` = 窗首初相,窗每拍前滑一点 → 纯音下每拍角**增大** + `2πf/Fs`;45~55 Hz 段 |Δφ| ≈ 0.28~0.35 rad,距 ±π 裕度巨大。 +- **定长窗为什么不够(自适应窗的理由)**:20 点矩形窗在非整周波时混入**负序泄漏项** + (45 / 55 Hz 处 |S₂/S₁| ≈ 3~5 %),相量端点轨迹成椭圆 → 逐拍相位差带上周期 `δ(φ)` + 纹波,纹波周期 = `fs/(2f)` ≈ 9.1~11.1 拍。**定长平均不整除纹波周期**,残差留在均值里: + 扫频最坏 0.317 Hz(40 拍算术平均)、0.197 Hz(60 拍复数和的 Kay 法——交叉项的和角不随 + `K²` 衰减,**勿再照这条路优化**)。改 `K_eff = round(8·Fs / (2·f̂))`(45~55 Hz 段 73~89 拍) + 使窗长恰为纹波周期的整数倍 → 纹波相消,扫频最坏降到 **0.0101 Hz**。 +- **假值钳制带取 35~65 Hz,而不是系统合理带**:单拍相位差本身带 ±5 Hz 纹波,钳制带贴边会 + **非对称丢弃纹波半周** → 均值偏置(实测 45 Hz 点 +0.16 Hz,已修)。35~65 Hz 宽于系统合理带 + 40.5~60.5 Hz(参考 `CN_HERTZ_SOFTFR_MIN/MAX`,见 + `docs/算法/继电保护功能复现规划.md` §10.1),仍能拦住建"窗滑 2 拍"造成的 ≈2f(90~110 Hz) + 与相位跳变假值;系统合理带由 OF / UF 的**定值范围**在消费侧把关,不在这里重复把关。 +- **幅值门** `FREQ_AMP_MIN = 50.0`(原始码值口径,**拟定值,待 L1-5 标定复核**):低于门视为 + 无有效波形(浮空 / PT 断线时 rms ≈ 0,相位无意义)→ 停止更新、`valid` 清零、读数保持, + 台架浮空因此**不发散**;信号刚消失的过渡段(≤20 ms 残窗)读数漂移有界 ≤0.3 Hz。 +- **源优先级 UA(C02) → IA(C01)**,按名前缀解析、每拍动态回退(台架只注电流也能测频); + **源切换即废相位基准重建**——UA 与 IA 之间没有共同相位基准(N-18),跨源相位差无意义。 + +### 7.2 精度判据(J1~J6,与 `test/host/freq/freq_test.c` 文件头同口径) + +| 判据 | 条件 | 门槛 | 当前实测最坏 | +|---|---|---|---| +| J1 扫频 | 纯音 45.00~55.00 Hz,0.5 Hz 步进(21 点) | **≤ 0.02 Hz** | **0.0101 Hz** @55.0 Hz | +| J2 噪声 | 幅值 1000 码、噪声 rms = 1 % 幅值 | **≤ 0.10 Hz** | 0.0160 Hz | +| J3 启动收敛 | 首个有效相量 → 首个有效读数 | **≤ 40 ms** | 20 ms | +| J4 频率阶跃 | 50 → 51 Hz 进入 ±0.05 Hz 并保持 | **≤ 120 ms** | 98 ms(稳态误差 0.0053 Hz) | +| J5 零幅 / 浮空 | rms < 幅值门 | `valid = 0`、不发散、保持漂移 ≤ 0.3 Hz | 漂移 0.13 Hz,无 NaN | +| J6 假值钳制 | 幅值 + 频率双跳变拍 | 60 ms 内回到 ±0.05 Hz | 0.0000 Hz | + +附加鲁棒性(同一套件):直流偏置 8000 码 → 0.0018 Hz;3 / 5 次谐波 20 % / 10 % → 0.0065 Hz; +UA 无幅值落 IA → 0.0020 Hz。 + +**J4 的 120 ms 是"精度优先于响应"的显式取舍**:自适应窗(M = 8 ≈ 4 个泄漏纹波周期 ≈ 4 个信号 +周波)换来 0.0101 Hz 量级的精度,代价是频率骤变后的窗冲刷(实测 98 ms)。OF / UF 定时限默认 +**1000 ms** → 测频响应占保护总动作时间 **< 10 %**,暂态假值另有 J6 兜底。**若将来要压响应时间, +必须同时重估精度**:缩短窗就是把精度退回 0.2~0.3 Hz 档,不是免费的。 + +--- + +## 8. L3 输入:保护定值模型 `prot_set.c` + +**解决的问题**:保护判据的"输入宪法"。参考系统把定值放 EEPROM + 常量表做校验和自检;本层做 +**RAM 模型 + 持久化纯逻辑**(blob 序列化 / 解析,文件胶住在 `App/cfg/prot_set_store.c`), +把**编号 / 通讯地址 / 范围 / 默认值 / 校验和**闭环钉死。 + +| API | 契约 | 为什么 | +|---|---|---| +| `prot_set_init()` | 装默认值 + 封存校验和;幂等 | 内核前可调 | +| `prot_set_load_raw(vals)` | 整表装载,逐项范围校验 | **all-or-nothing**:任一项越界则**整表不装载**,`prot_set_bad_mask()` 指认越界项;影子区 + 单字交换 → 读侧永不见中间态 | +| `prot_set_set(id, q10)` | 单项写入(范围校验,批处理路径) | **不自动重封存**——批量改完必须 `prot_set_seal()`,否则 selfcheck 判"非法改写" | +| `prot_set_set_sealed(id, q10)` | **原子写 + 封存**:影子区改好重封后单字交换 | 自检永不见中间态(收口 B5 发现的竞态窗口);置脏标志 | +| `prot_set_dirty_peek()` / `_take()` / `_mark()` | 脏标志三件套(落盘握手) | peek = 通知 iec104Task;take = 属主落盘前消费;mark = 写失败回置重试 | +| `prot_set_store_serialize()` / `_parse()` | 持久化 blob:`PSET` + 版本 + 值区 + sum32 正 / 反码对 | parse **整表原子**:magic / 版本 / 长度 / 校验对 / 范围任一失败 = **一字不动** | +| `prot_set_get_f()` / `get_ctl()` | 工程量浮点 / 投退读取 | 单字读原子 | +| `prot_set_selfcheck()` | 重算 sum32 与封存正 / 反码比对,失配 → 闭锁(L5-1) | seqlock 式复核:提交时撞见改写 = 本轮跳过而**不误锁**;`prot_set_seal()` 是解锁的**唯一**合法路径 | + +**要点** + +- **Q10 定点**(×1024)与参考系统 `2^10` 折算口径一致;元数据表(地址 / 范围 / 默认)是 ROM + 常量,**校验和只覆盖 RAM 值区**。 +- **通讯地址逐项对照参考** `Comm/Pub/Const_CommSetPara.c` 的 `g_tCommSetParaTab`(基址 + `0x8820`);单测 TC-PROTSET-006 钉死"地址不重复、`min ≤ def ≤ max`、投退项 0..1"。 +- ⚠ **默认值与范围是拟定值**(参考 EEPROM 出厂表不在本仓库),待对照修订——进度注见 + `docs/算法/继电保护功能复现规划.md` §5.4。 +- **OF / UF 八项尾追加**(B8,32 → 40 项):`PS_OF_EN / OF_HZ / OF_DLY / UF_EN / UF_HZ / UF_DLY + / RET_OF / RET_UF`——**编号不回收,枚举序 = 数组序 = 校验和覆盖序**,故只许尾部追加。 + 这八项的逐项通讯地址不在本仓库(参考表只示"含低压 / 低频"类目)→ 全部按 **本地项 + `comm_addr = 0`** 登记(104 C_SE 不可寻址),待对照补。 +- **频率返回系数独立**:`RET_OF = 0.99` / `RET_UF = 1.01`(返回门槛 = 定值 × 系数)—— + 频率滞回带与电气量的 0.95 / 1.05 不同量级,**勿共用一个返回系数**。 +- **换代语义**(B7 → B8):项数变 → blob 144 → 176 B、`PROT_SET_BLOB_VER` 1 → 2。旧档(ver = 1 / + 32 项)在新固件 parse 时被**版本 / 长度闸**拒绝 → 回落出厂默认,`prot_set_store_load()` + **返回 2 = 换代**(打 INFO,**不是** CORRUPT、不记自检),`-1` 才是真损坏。 + 升级路径**不得**报成损坏(判读细节见 §12.3)。 +- **bad_mask 随项数扩为 `uint64_t`**(bit i = 第 i 项越界);消费方目前只有主机单测, + 板端闭锁走 `prot_set_locked()`。 + +--- + +## 9. L3 保护元件框架 `prot_elm.c` + +**解决的问题**:参考系统 89 个 `PRS_*.c` 各自实现"滞回 + 定时限"——本框架把这段公共核收敛成 +**一个通用核 + 数据驱动实例表**(与 `phasor` 的 `k_inst` 同一模式):新增元件 = 表加一行 + +定值加几项。 + +| API | 契约 | +|---|---| +| `prot_elm_init()` | 复位状态 + 按名前缀(`C01/C02/C03`)解析相量实例索引;须在 `phasor_init()` 之后;`src = FREQ` 行无相量绑定(`pidx = 0xFF`) | +| `prot_elm_eval(now_ms, meas)` | **纯判据**:定值 RAM + 测量数组 → 全元件状态机;主机单测入口;`NULL` 测量按 0 | +| `prot_elm_run(now_ms)` | 适配层:按实例表 `src` 组测量(`0` 取 `phasor_get()` rms,`1` 取 `freq_get()`)再 eval | +| `prot_elm_state()` / `prot_elm_active_mask()` | 状态快照 / 动作位图,供 L4 出口与心跳日志 | +| `prot_elm_request_reset()` | 跨上下文复归入口(104 YK11 落请求,`eval` 头部消费) | + +**要点** + +- **滞回**:已启动用**返回**门槛(定值 × 返回系数),未启动用**动作**门槛;过量 / 欠量方向由实例表 + `over` 位统一处理(`UV` / `UF` 是欠量实例)。 +- **定时限**用差值法 `(uint32_t)(int32_t)(now - t0) >= dly` → tick 回绕安全;`dly = 0` = + 启动同拍即动作。**返回是即时的**(参考系统带 10 ms 返回延时,接出口硬件时再补)。 +- **闭锁联动**:`prot_set_locked()`(L5-1)或该元件投退 = 0 → 状态整体复位、不再判据。 +- **数据缺失时的两种处理刻意不同**(这是本层最容易改错的一处): + - 相量类**任一未就绪 → 整拍跳过**(不给判据喂陈旧值); + - 频率类**无有效读数 → 按方向填"必不启动"值**(过量填 `0`、欠量填 `1e30` → 滞回比较恒假 → + 元件复位),**不整拍跳过**——频率源缺失不得饿死电流 / 电压类元件。 +- **V0.1 实例表**(受 N-18 约束):`OC1 / OC2 / OC3 / OL`(IA)、`OV / UV`(UA)、`3U0` + (UN 外部通道)、`OF / UF`(`freq` 测频,B8)。**零序过流(IN 走 STM32)故意不在表中** + ——跨转换器不构成同刻矢量。 +- **OF / UF 的出口可见性**:两元件**未单独映射 104 遥信点**(送检点表口径见 + `docs/算法/继电保护功能复现规划.md` §7),动作目前**只经 YX15 保护总聚合可见** + (`iec104_pts` 的 fault-total 掩码含 OC / 3U0;OF / UF 置总位但无独立分点)。 + **文档与代码都必须按此口径,勿写"独立遥信"。** +- **板端接线**:`App/task/protect_task.c`(protectTask,20 ms)消费 `prot_elm_run()` + + L5-4 采样故障闭锁 + L5-1 校验和巡检;判据结果经 104 遥信上送(`iec104_pts` YX15~23 / 31) + → `docs/算法/继电保护功能复现规划.md` §7。实例数变化会同时改变启动锚点 `elm=` 数字与 + `prot_elm_active_mask()` 位宽(B8:7 → 9 实例),串口锚点表若含该数字须同步核对。 + +--- +## 10. 关键约定与禁令 + +1. **语义映射只有一处权威**:`docs/硬件平台/ADC通道映射说明.md`。取数一律经 `adc_input_raw()`, + **禁止**在消费侧直接索引驱动缓冲或写死下标。 +2. **禁止跨转换器运算**(V0.1 硬件缺陷 **N-18**):`UA/UN/IA` 在 TPAFE、`UB/UC/IB/IC/IN` 在 + STM32 ADC1,两片转换器没有共同采样触发 → 无同一时刻性、相位不确定。故本层**不做**跨转换器的 + 相位 / 功率 / 功率因数 / 负序零序类矢量运算;**8 路全接 TPAFE(V0.2 硬件)后方可解禁**。 +3. **标定未实现**(缺标准源,见 `docs/算法/继电保护功能复现规划.md` §3.1 L1-5):现以**原始码值** + 充当工程量(系数 = 1)→ 相量读数只证明**链路通**,**不可作幅值精度判据**。 +4. **本层无副作用外溢**:不建任务、不等事件、不写日志(日志由调用侧打)、不碰寄存器 + (例外只有 `adc_input.c` 驱动 ADC1)。 +5. **上下文纪律**:内核启动前只允许调 `*_init()`;`adc_input_poll()` / `phasor_run()` / + `freq_run()` / `prot_elm_run()` 只在各自任务里调。 +6. **表只许尾部追加**:`k_inst[]`(相量实例)、`prot_elm` 实例表、定值元数据表—— + 枚举序 = 数组序 = 校验和覆盖序,插入或重排等于改校验和与 104 点表。 + +--- + +## 11. 资源足迹(改容量前先看这里) + +| 位置 | 量 | 说明 | +|---|---|---| +| `sample_ring.c` `s_ring[9][40]`(`float`) | **1440 B** | 周波数 / 通道数一改就成倍变化 | +| `phasor.c` `s_zone[2][5]`(`phasor_t`) | 160 B | 随实例数线性 | +| `freq.c` 相位增量环 `fq_d[96]` + 状态 | **≈ 520 B** | 随 `FREQ_AVG_TAPS` 变化 | +| `prot_set.c` `s_val[2][40]` + 封存 / `uint64_t` 位图 / 闭锁 | 336 B | 项数 32 → 40(B8)后的量 | +| `prot_elm.c` `s_rt[9]`(16 B / 实例) | 144 B | 实例 7 → 9(B8)后的量 | +| `adc_input.c` `s_stm32_raw[16]` + `s_tpafe_raw[8]` | 48 B | — | +| `adc_input.c` `s_stm32_dma[2×9]`(`uint16_t`) | 36 B | 必须在 `RW_IRAM1`(DMA2 不可达 CCM)→ `docs/子系统设计/ADC采样节拍设计说明.md` §4.3 | +| `adc_health.c` 内部状态 | ≈ 40 B | — | +| 运行期栈 `phasor_run()` 的 `win[20]` | 80 B / 帧 | 抬高 `PHASOR_N` 直接抬栈 | +| 运行期栈 `freq_run()` 局部 | ≈ 30 B / 帧 | 无大局部数组 | + +**改 `SAMPLE_RING_CYCLES` / 实例数 / `ADC_SEM_COUNT` / `FREQ_AVG_TAPS` / 栈之前后各跑一次** +`scripts/stack_report.py`,新增缓冲跑 `scripts/ram_report.py`(口径见 `mem-budget` skill); +adcTask 静态最坏链与余量的**台账在** `docs/内存管理/RAM使用分析与优化方案.md` §2.3,本表不代记台账。 + +--- + +## 12. 验证与判读 + +### 12.1 主机侧单测(生产源文件以 `#include` 原样纳入,不改产品代码) + +| 模块 | 套件钉死的东西 | 细则 | +|---|---|---| +| `phasor` / `sample_ring` | 系数表逐项复核、幅值与相位口径、谐波抑制与提取、频偏、采样环时序、双区交换 | `docs/测试说明/相量层单元测试说明.md` | +| `adc_input` | 逐 Rank 注入互异码值钉死映射、HAL 启动失败与"本拍无新帧"分支、乒乓半区推导、gap / stall 分开计数 | `docs/测试说明/ADC输入层单元测试说明.md` | +| `adc_health` | 100 ms 静默门限两侧、丢帧边沿、序号倒退锁存、错误增量、tick 回绕、`NULL` 守卫、`init` 幂等 | `test/host/adc_health/` | +| `prot_set` | 范围校验、all-or-nothing 装载、封存-自检-闭锁、Q10 往返、元数据表自检、blob 往返与损坏拒绝、换代拒绝、`uint64_t` bad_mask | `test/host/prot_set/` | +| `prot_elm` | 滞回与定时限、欠量方向、投退、闭锁联动、时基回绕、位图聚合、测频接线与"必不启动值" | `test/host/prot_elm/` | +| `freq` | J1~J6(§7.2)+ 直流与谐波抑制 + UA→IA 源回退 + 守卫与幂等 | `test/host/freq/` | + +命令与对应 pre-commit 钩子名见 `App/algo/README.md` §4(钩子清单的属主是 +`.pre-commit-config.yaml`,用例清单的属主是各 `test/host/<模块>/` 与上表细则文档——本文不复制数字, +免得三处打架)。 + +### 12.2 板端闭环 + +`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍与心跳:`phasor(...)` 行、`freq(...)` 行 +(`f=5000 valid=1`,`×0.01` 定点,MicroLIB 无 `%f`)、`ring` / `skip` / `adc1_*` / `tgap` +计数;健康判据 = **零 `[ERR]`**(`adc_health_report()` 的告警全部以 `[ERR]` 开头 → 误报会被闭环 +直接抓住)。浮空工况应为 `valid = 0` 且读数为保持值。 + +### 12.3 定值档"换代"与"损坏"是两件事(易误判) + +- `prot_set_store_load()` **返回 2 = 换代**(旧固件 / 旧项数的档被版本闸拒绝)→ 只打 INFO + `prot set blob version change, defaults in use`,**不记自检**; +- **返回负值 = 真损坏** → `[ERR] prot set blob CORRUPT` + 自检留痕。 + +升级固件后首启看到 "version change" 一行属**预期**,不要当故障排查;反过来,把真损坏读成 +换代等于放过 NAND 数据事故——分流依据是返回码,不是日志文本。 + +--- + +## 13. 相关文档 + +- 目录索引与构建 / 测试入口:`App/algo/README.md` +- 分层与功能项清单(L1~L5 的属主登记处):`docs/算法/继电保护功能复现规划.md` +- 通道映射权威口径:`docs/硬件平台/ADC通道映射说明.md` +- 采样节拍与 DMA 搬运:`docs/子系统设计/ADC采样节拍设计说明.md` +- 傅氏算法与有效值口径:`docs/算法/傅氏算法详解.md` §2.3 +- 主机单测细则:`docs/测试说明/相量层单元测试说明.md`、`docs/测试说明/ADC输入层单元测试说明.md` +- RAM 与栈台账:`docs/内存管理/RAM使用分析与优化方案.md` §2.3 diff --git a/test/host/freq/freq_test.c b/test/host/freq/freq_test.c index 344be2ec..6dc4a52e 100644 --- a/test/host/freq/freq_test.c +++ b/test/host/freq/freq_test.c @@ -7,7 +7,7 @@ * ../../../App/algo/phasor.c * ../../../App/algo/freq.c * - * 精度判据(与 App/algo/README.md §3.7 同口径,规划 §4.2 测频范围的落实): + * 精度判据(与 docs/算法/采集与算法层设计说明.md §7.2 同口径,测频范围见规划 §4.2): * J1 纯音扫频 45.00~55.00 Hz(0.5 Hz 步进):|f_meas - f_true| <= 0.02 Hz * (实测最坏 0.0101 Hz @55.0 Hz,判据留 2 倍裕度) * J2 噪声工况(幅值 1000 码、噪声 rms = 1% 幅值):|f_meas - f_true| <= 0.10 Hz diff --git a/test/host/freq/run_unit_test.py b/test/host/freq/run_unit_test.py index b2360439..ad81f08d 100644 --- a/test/host/freq/run_unit_test.py +++ b/test/host/freq/run_unit_test.py @@ -5,7 +5,7 @@ 做什么:用本机 MSVC 离线编译 test/host/freq/freq_test.c(#include 纳入生产 源文件 App/algo/freq.c + phasor.c + sample_ring.c),验证相位解缠绕 测频的精度判据 J1~J6(45~55 Hz 扫频 / 噪声 / 收敛 / 阶跃 / 浮空 / - 假值钳制 / 源回退),判据数字见 freq_test.c 文件头与 App/algo/README.md。 + 假值钳制 / 源回退),判据数字见 freq_test.c 文件头与 docs/算法/采集与算法层设计说明.md §7.2。 用法(仓库根目录): & '.venv\\Scripts\\python.exe' test/host/freq/run_unit_test.py