# App/algo/ — 采集与算法层(L1 / L2) > **自研代码**,属 `docs/算法/继电保护功能复现规划.md` §2.1 分层中的 **L1(采集与预处理)** > 与 **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 的 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-6(软件侧) | 依赖关系(自下而上,无环):`adc_input.h`(纯枚举 + 声明)← `sample_ring` ← `phasor`; `prot_set` **独立**(仅 `stdint.h`);`prot_elm` ← `prot_set` + `phasor`。 `sample_ring.c` / `phasor.c` / `adc_health.c` / `prot_set.c` / `prot_elm.c` **零平台依赖** (只用 `math.h` / `stddef.h` / `stdint.h` 与本目录头); `adc_input.c` 另依赖 `adc.h`(`hadc1`)与 `tpafe5160.h`(快照),主机侧以桩头替身。 ## 2. 数据流(一拍 = TIM3 1 kHz 节拍) ```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 --> 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_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`——四类成因不同,**勿混读** | **要点** - 语义映射表 `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` 那种零依赖形态,才能免桩单测。 ### 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-PS-006 保证地址不重复、`min ≤ def ≤ max`、投退项 0..1。 - ⚠ **默认值/范围为拟定值**(参考 EEPROM 出厂表不在本仓库),待对照修订—— 见规划 §5.4 落实进度注。 ### 3.6 `prot_elm.c` / `prot_elm.h` — 保护元件框架(L3,P1 子集) **解决的问题**:参考系统 89 个 `PRS_*.c` 各自实现"滞回 + 定时限"——本框架把这段 公共核收敛为**一个通用核 + 数据驱动实例表**(同 `phasor` 的 `k_inst` 模式), 新增元件 = 表加一行 + 定值加几项。 | API | 干什么 | 注意 | |---|---|---| | `prot_elm_init()` | 复位状态 + 解析相量实例索引(按名前缀 `C01/C02/C03`) | 须在 `phasor_init()` 之后调 | | `prot_elm_eval(now_ms, meas)` | **纯判据**:定值 RAM + 测量数组 → 全元件状态机 | 主机单测入口;NULL 测量按 0 | | `prot_elm_run(now_ms)` | 适配层:`phasor_get()` 组测量 → eval | 任一相量未就绪 → **整拍跳过**(不引入陈旧判据) | | `prot_elm_state()` / `prot_elm_active_mask()` | 状态快照 / 动作位图 | 供 L4 出口与心跳日志 | **要点** - **滞回**:已启动用返回门槛(`定值 × 返回系数`),未启动用动作门槛—— 过量/欠量方向由实例表 `over` 位统一处理(`UV` 是唯一欠量实例)。 - **定时限**:`(uint32_t)(int32_t)(now - t0) >= dly` 差值法,tick 回绕安全; `dly=0` → 启动同拍即动作。返回**即时**(参考带 10 ms 返回延时,P4 接出口硬件时再补)。 - **闭锁联动**:`prot_set_locked()`(L5-1)或投退=0 → 该元件状态整体复位、不判据。 - **V0.1 实例**(N-18):`OC1/OC2/OC3/OL`(IA)、`OV/UV`(UA)、`3U0`(UN 外部通道)。 零序过流(IN 走 STM32)与过频/低频(待频率算法)**故意不在表中**。 - **板端接线已落地(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 ## 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[31]` + 封存/位图/闭锁 | 136 B(**未接线前被链接器移除,实测 0**) | | `prot_elm.c` `s_rt[7]`(16 B/实例) | 112 B(同上,实测 0) | | 运行期栈:`phasor_run()` 的 `win[20]` | 80 B/帧 | 改容量(`SAMPLE_RING_CYCLES` / 实例数 / `ADC_SEM_COUNT`)前后各跑一次 `& '.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` | | 有效性校验(31 用例:100 ms 静默门限两侧 / 丢帧边沿 / 倒退锁存 / 错误增量 / tick 回绕 / NULL 守卫 / init 幂等) | `& '.venv\Scripts\python.exe' test/host/adc_health/run_unit_test.py` | `adc-health-unit-test` | | 定值模型(30 用例:范围校验 / all-or-nothing 装载 / 封存-自检-闭锁 / Q10 往返 / 元数据表自检) | `& '.venv\Scripts\python.exe' test/host/prot_set/run_unit_test.py` | `prot-set-unit-test` | | 元件框架(27 用例:滞回与定时限 / 欠量方向 / 投退 / 闭锁联动 / 时基回绕 / 位图聚合) | `& '.venv\Scripts\python.exe' test/host/prot_elm/run_unit_test.py` | `prot-elm-unit-test` | 改本目录任一 `.c/.h`,提交时对应钩子自动跑;详见 `docs/测试说明/相量层单元测试说明.md`。 - **板端**:`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍/心跳日志(`phasor(...)` 行、 `ring` / `skip` 计数、`adc1_rc` / `adc1_err`、`tgap`);健康判据 = 零 `[ERR]` (`adc_health_report` 的告警全部以 `[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` - 分层与路线图(L1/L2 清单与状态):`docs/算法/继电保护功能复现规划.md` - 傅氏算法与有效值口径:`docs/算法/傅氏算法详解.md` - 采样节拍(TIM3 1 kHz + TRGO 触发 ADC1/DMA):`docs/子系统设计/ADC采样节拍设计说明.md` - 相量层单测细则:`docs/测试说明/相量层单元测试说明.md`