From 317e97fca6e687bda5a2b2aee3c560b91a223028 Mon Sep 17 00:00:00 2001
From: Wanderingss <1624155937@qq.com>
Date: Sat, 10 Oct 2026 18:04:51 +0800
Subject: [PATCH] =?UTF-8?q?docs(algo):=20App/algo/README.md=20=E8=A1=A5?=
=?UTF-8?q?=E9=BD=90=E4=B8=BA=E8=AF=A6=E7=89=88=EF=BC=88=E5=88=86=E5=B1=82?=
=?UTF-8?q?/=E6=95=B0=E6=8D=AE=E6=B5=81/=E6=8E=A5=E5=8F=A3/=E6=B5=8B?=
=?UTF-8?q?=E8=AF=95/=E6=89=A9=E5=B1=95=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 该文件此前仅 27 行(仅"现有模块"表 + 约定 + 相关文档),本次补齐为 168 行
详版:文件清单、1 ms 数据流图、逐文件 API 表与要点、关键约定、静态足迹、
构建与测试(含两个主机单测钩子)、新增算法模块的固定动作。
- 内容已逐项对照当前源码核验:相量实例 5 行 / 环容量 2×20 点 / 9 个语义通道 /
Rank 映射(CH0..CH7 + CH14)/ 所引文档、脚本与 skill 均存在。
- 与 L1/L2 现状一致:adc_input(L1)、sample_ring + phasor(L2)三模块。
---
App/algo/README.md | 173 ++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 157 insertions(+), 16 deletions(-)
diff --git a/App/algo/README.md b/App/algo/README.md
index 54375b2b..dbe41c0b 100644
--- a/App/algo/README.md
+++ b/App/algo/README.md
@@ -1,27 +1,168 @@
# App/algo/ — 采集与算法层(L1 / L2)
> **自研代码**,属 `docs/算法/继电保护功能复现规划.md` §2.1 分层中的 **L1(采集与预处理)**
-> 与 **L2(相量计算,拟)**。原则:**算法与 I/O 分离**——本层不碰寄存器、不碰任务调度,
-> 只做"数"(码值 → 工程量)与"相量"(工程量 → 复相量),从而可成为主机侧单元测试
-> (L0.5)的天然适格对象(落实登记簿 **A-4**)。
+> 与 **L2(相量计算)**。原则:**算法与 I/O、与调度分离**——本层不建任务、不做保护判据、
+> 不碰寄存器(唯一例外见 §3.1),只做"数"(原始码值 → 语义通道)与"相量"
+> (采样序列 → 复相量),因而可被主机侧单元测试(L0.5)离线全覆盖(落实登记簿 **A-4**)。
+>
+> 任务侧(`App/task/adc_task.c`)只做**消费与编排**;任何新算法一律落在本目录,勿塞进 `App/task/`。
-## 现有模块
+## 1. 文件清单(一眼版)
-| 文件 | 层 | 职责 |
+| 文件 | 层 | 作用(一句话) | 主要对外入口 | 覆盖规划项 |
+|---|---|---|---|---|
+| `adc_input.h` / `adc_input.c` | L1 | **统一语义取数层**:把两片转换器的原始码值按语义(`UA…IN`/温度)暴露,并驱动 STM32 ADC1 9 通道顺序扫描 | `adc_input_init()` `adc_input_poll()` `adc_input_raw()` | L1-2 / L1-3 / L1-4 |
+| `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 |
+
+依赖关系(自下而上,无环):`adc_input.h`(纯枚举 + 声明)← `sample_ring` ← `phasor`。
+`sample_ring.c` / `phasor.c` **零平台依赖**(只用 `math.h` / `stddef.h` 与本目录头);
+`adc_input.c` 另依赖 `adc.h`(`hadc1`)与 `tpafe5160.h`(快照),主机侧以桩头替身。
+
+## 2. 数据流(一拍 = 1 ms)
+
+```mermaid
+flowchart TD
+ TIM6["TIM6 更新中断 1 kHz
Src/main.c → tpafe5160_start_conv_irq()"] --> TPAFE["TPAFE5160 转换
BUSY EXTI 收帧"]
+ TPAFE --> POLL
+ TASK["adcTask(1 ms osDelayUntil)
App/task/adc_task.c"] --> POLL["adc_input_poll()
TPAFE 快照 + STM32 ADC1 9 通道扫描"]
+ POLL --> RAW["adc_input_raw(ch) × 9
按语义取原始码值"]
+ RAW --> RING["sample_ring_push_frame()
整帧入环(40 点窗)"]
+ RING --> RUN["phasor_run()
逐实例 20 点重算 + 双区切换"]
+ RUN --> GET["phasor_get(idx)"]
+ GET --> CONS["消费者:adcTask 日志(现状)
UI / 自检 / L3 保护任务(待接)"]
+```
+
+初始化落点:`adc_input_init()` 与 `phasor_init()` 由 `App/app/app_main.c` 的
+`app_main_init()` 在**内核启动前**调用(三者 `*_init()` 均无 RTOS 依赖、幂等);
+`phasor_init()` 内部会连带 `sample_ring_init()`,等价于"刚上电"。
+
+## 3. 逐文件说明
+
+### 3.1 `adc_input.c` / `adc_input.h` — L1 ADC 输入层
+
+**解决的问题**:V0.1 上 8 路电气量被两片转换器分片(硬件缺陷 **N-18**),且网名 `AINn`
+≠ 板级 `ADCn` ≠ 转换器通道号(13/17 路置换)。若消费侧直接索引驱动缓冲,改版即全线错位。
+本层把"哪一路"收进一张表,上层**按语义取数、不写死下标**。
+
+| API | 干什么 | 注意 |
|---|---|---|
-| `adc_input.c/h` | L1 | **ADC 输入层**:① **语义通道映射**(L1-4)——`UA/UB/UC/UN/IA/IB/IC/IN/温度` → 物理通道,消费侧**按语义取数、不写死下标**;② **STM32 ADC1 9 通道软件触发顺序扫描**(L1-3);③ TPAFE5160 原子快照消费 |
+| `adc_input_init()` | 清缓冲与统计 + **映射表自检**(每个 STM32 语义通道必须落在扫描 Rank 序列内) | 内核启动前调用;返回 `-1` = 映射指向了从不扫描的通道(会恒读陈旧值) |
+| `adc_input_poll()` | 采一拍:`tpafe5160_snapshot()` 原子快照(8 通道)+ STM32 ADC1 9 通道软件触发顺序扫描 | **仅任务上下文**(快照走临界区);只搬码值,不做标定/运算 |
+| `adc_input_raw(ch, &out)` | 按语义读原始码值(TPAFE 16 位补码 / STM32 0..4095) | 消费侧取数**唯一入口**;须先成功 `poll()`;`-2` = 该来源尚无有效数据 |
+| `adc_input_desc(ch)` | 语义 → 物理通道描述(来源 + 下标/通道号) | 供自检注入、UI 通道序;返回静态地址,勿改 |
+| `adc_input_stm32_rc()` / `adc_input_stm32_errs()` | STM32 侧最近扫描状态码 / 累计失败次数 | 心跳日志观测面(`adc1_rc` / `adc1_err`) |
-## 关键约定
+**要点**
-- **语义映射的唯一权威口径**是 `docs/硬件平台/ADC通道映射说明.md`(V0.1 有 13/17 路被置换,
- 网名编号 ≠ 通道号)。V0.1 上 8 路电气量分属两片转换器(硬件缺陷 **N-18**):`UA/UN/IA`
- 在 TPAFE5160、`UB/UC/IB/IC/IN` 在 STM32 ADC1。**V0.2 改版只改映射表内容**,本层 API
- 与上层语义不动。
-- **取数一律经 `adc_input_raw()`**(按语义),禁止在消费侧直接索引驱动缓冲或写死下标。
-- 本层**不做**跨转换器的相位 / 功率 / 功率因数 / 矢量类运算(V0.1 硬件缺陷 N-18 限制)。
+- 语义映射表 `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 个通道**不是同一时刻**;单通道轮询超时 2 ms,超时判失败、绝不拖垮消费节拍。
+- 本文件是本层**唯一**允许出现 HAL 调用的地方(它必须驱动 ADC1);新增模块若不需要外设,
+ 请保持 `sample_ring` / `phasor` 那种零依赖形态,才能免桩单测。
-## 相关文档
+### 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)`。
+
+## 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 |
+| 运行期栈:`phasor_run()` 的 `win[20]` | 80 B/帧 |
+
+改容量(`SAMPLE_RING_CYCLES` / 实例数 / `ADC_SEM_COUNT`)前后各跑一次
+`& '.venv\Scripts\python.exe' scripts/stack_report.py`(口径见 `mem-budget` skill)。
+
+## 6. 构建与测试
+
+- **挂载**:三个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`,`../App/algo` 已在 `` 内。
+ 本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路)。
+- **主机侧单测(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` |
+
+ 改本目录任一 `.c/.h`,提交时对应钩子自动跑;详见 `docs/测试说明/相量层单元测试说明.md`。
+- **板端**:`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍/心跳日志(`phasor(...)` 行、
+ `ring` / `skip` 计数、`adc1_rc` / `adc1_err`)。
+
+## 7. 新增一个算法模块的固定动作
+
+1. 在本目录建 `xxx.c/h`,文件头写模块注释块(规范 V1.0),**尽量零平台依赖**以便免桩单测。
+2. 用 `new-source-file` skill 注册到 `MDK-ARM/STM32F407-Demo.uvprojx`(目录已在 IncludePath,
+ 若新建子目录则必须补 IncludePath)。
+3. 加 `test/host/<模块>/` 主机单测 + `.pre-commit-config.yaml` 钩子(照 `phasor-unit-test` 抄)。
+4. 消费者写在 `App/task/`,本层只出"数";跨转换器运算的禁令见 §4。
+
+## 8. 相关文档
- 通道映射权威口径:`docs/硬件平台/ADC通道映射说明.md`
-- 分层与路线图:`docs/算法/继电保护功能复现规划.md`(§2.1 分层、§3.1 L1 清单)
-- 采样节拍:`docs/子系统设计/ADC采样节拍设计说明.md`
+- 分层与路线图(L1/L2 清单与状态):`docs/算法/继电保护功能复现规划.md`
+- 傅氏算法与有效值口径:`docs/算法/傅氏算法详解.md`
+- 采样节拍(TIM6 1 kHz):`docs/子系统设计/ADC采样节拍设计说明.md`
+- 相量层单测细则:`docs/测试说明/相量层单元测试说明.md`