批 2(用户授权改名 + 「新名要一眼看懂」裁定)
- 16 个有歧义模块码单词化,共 1,146 处随 scripts/rename_ids.py 改写:
AI→ADCIN、AH→ADCHEALTH、PS→PROTSET、PE→PROTELEM、PH→PHASOR、
TS→TIMESYNC、VL→VOLLOCK、NP→NANDPORT、CFG→CONFIG、CLK→SYSCLK、
WD→WATCHDOG、STO→STORAGE、SH→SHA256、FA→FTPAUTH、FR→FREQ、104→IEC104
- 不动 NET/GD5F/RTC/SOE/UI/ADC/LOG/RS485/TOOL(已是单词、芯片名或业界通用词,
改了纯属 churn)
- 别名按全局登记:这批字面量已完全限定(TC-<码>-<nnn>),不存在裸形多义
- 映射表存档 scripts/rename_maps/batch2.json 作审计轨迹;§6 模块码表同步
连带修门禁(关键教训)
- 模块码涨到 9 位(ADCHEALTH/WATCHDOG)后,候选正则与族正则的 {0,4} 上界
会让改名后的号**整批从门禁眼里消失**:实测编号字面量从 8271 掉到 7315、
悬空假降 490 处。已放宽到 {0,9} 并写进 §6 注记
- 结论:改名批次必须连带复核识别面,只改数据不改判据 = 静默放行;
这次是靠「可判定比例跌破基线即失败」那条线拦下来的
现状与遗留
- 校验 1 未登记前缀 0 处;可判定比例 82.71%(基线 82.6%,只涨不跌成立)
- 遗留:7 个模块的「单元测试说明.md」仍缺(PROTSET 116、NANDPORT 109、
PROTELEM 65、FREQ 48、FTPAUTH 45、SHA256 25、SOE 14 处悬空)——改名不解决
缺文档,处置方式待议
- 遗留:T-2/T-3/T-4/T-6 历史任务号悬空(别名表未收全,属本工具批次漏项)
- 未改任何 C/H 可编译语义(仅注释与宏名内的编号),故不需 @build
30 KiB
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-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(纯枚举 + 声明)← 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(快照),主机侧以桩头替身。
2. 数据流(一拍 = TIM3 1 kHz 节拍)
flowchart TD
TIM3["TIM3 更新事件 1 kHz(PSC 83 / ARR 999)<br/>ISR: tpafe5160_start_conv_irq() + adc_tick_irq_notify()<br/>同一事件经 TRGO 触发 ADC1"] --> TPAFE["TPAFE5160 转换<br/>BUSY EXTI 收帧"]
TIM3 --> ADC["STM32 ADC1 一轮 9 通道(≈67 us)<br/>DMA2_Stream4 循环模式自动搬运"]
ADC --> PP["乒乓双缓冲 s_stm32_dma[2×9]<br/>半传输 / 传输完成回调各推进 seq"]
TIM3 -. "ADC_WAKE_FLAG<br/>osThreadFlagsWait" .-> TASK
TPAFE --> POLL
PP --> POLL
TASK["adcTask(事件驱动唤醒,非定时轮询)<br/>App/task/adc_task.c"] --> POLL["adc_input_poll()<br/>TPAFE 快照 + 消费刚完成的那半区(零忙等)"]
POLL --> RAW["adc_input_raw(ch) × 9<br/>按语义取原始码值"]
RAW --> RING["sample_ring_push_frame()<br/>整帧入环(40 点窗)"]
RING --> RUN["phasor_run()<br/>逐实例 20 点重算 + 双区切换"]
RUN --> FREQ["freq_run()<br/>共轭乘积相位增量 + 自适应窗均值(B8)"]
RUN --> GET["phasor_get(idx)"]
FREQ --> FGET["freq_get() / freq_valid()"]
GET --> CONS["消费者:adcTask 日志(现状)<br/>UI / 自检 / L3 保护任务(待接)"]
FGET --> PCONS["消费者:prot_elm_run() OF/UF 判据(B8)<br/>adcTask 心跳 freq 行"]
初始化落点:adc_input_init() 与 phasor_init()、freq_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→ TPAFEbuf[6]/[5]/[2];UB/UC/IB/IC/IN/TEMP→ ADC1CH14/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(五次谐波选线)。 新增实例只在表尾加一行,遍历/日志/单测自动适配。 - 双区(计算区 / 使用区):结果先写非使用区,全部实例写完后单字节
volatilestore 切换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.cg_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_VER1→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_ptsYX15~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)=窗首初相,窗每拍前滑一点); 4555 Hz 段 |Δφ| ≈ 0.280.35 rad,距 ±π 裕度巨大。 - 自适应窗长(核心):20 点矩形窗在非整周波时混入负序泄漏项
(45/55 Hz 处 |S₂/S₁| ≈ 3
5%),相量端轨迹成椭圆 → 逐拍相位差带周期 δ(φ) 纹波,周期 = fs/(2f) ≈ 9.111.1 拍。定长窗平均不整除纹波周期 → 扫频最坏误差 0.317 Hz(40 拍算术平均)/ 0.197 Hz(60 拍复数和 Kay, 交叉项和角不随 K² 衰减,勿再尝试)。改K_eff = round(8·Fs/(2·f̂))(4555 Hz 段 7389 拍)使窗长恰为纹波周期整数倍 → 纹波相消,最坏 0.0101 Hz。 - 假值钳制带 35~65 Hz(宽于系统合理带 40.5
60.5 = 参考65 仍拦得住窗滑 2 拍的 ≈2f (90~110 Hz)与相位跳变假值;系统合理带由 OF/UF 定值范围在消费侧把关。CN_HERTZ_SOFTFR_MIN/MAX, 规划 §10.1):单拍相位差本身带 ±5 Hz 纹波,钳制带贴边会非对称丢弃纹波半周 → 均值偏置(实测 45 Hz 点 +0.16 Hz,已修)。35 - 幅值门
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已在<IncludePath>内。 本层不进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.pyphasor-unit-testADC 输入层(逐 Rank 注入互异码值钉死映射 + HAL 启动失败/超时分支) & '.venv\Scripts\python.exe' test/host/adc_input/run_unit_test.pyadc-input-unit-test有效性校验(31 用例:100 ms 静默门限两侧 / 丢帧边沿 / 倒退锁存 / 错误增量 / tick 回绕 / NULL 守卫 / init 幂等) & '.venv\Scripts\python.exe' test/host/adc_health/run_unit_test.pyadc-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.pyprot-set-unit-test元件框架(47 用例:滞回与定时限 / 欠量方向 / 投退 / 闭锁联动 / 时基回绕 / 位图聚合 / B8 OF-UF 滞回与方向隔离 + run() 测频接线) & '.venv\Scripts\python.exe' test/host/prot_elm/run_unit_test.pyprot-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.pyfreq-unit-test(待登记,合并时由 Lead 加钩子)改本目录任一
.c/.h,提交时对应钩子自动跑;详见docs/测试说明/相量层单元测试说明.md。 -
板端:
@build0 错误 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 = 换代 (旧固件/旧项数的档被版本闸拒绝)→ 只打 INFOprot set blob version change, defaults in use, 不是[ERR] ... CORRUPT、不记自检;rc<0 = 真损坏 →[ERR] prot set blob CORRUPT+ 自检留痕。 升级固件后首启看到"version change"一行属预期,勿当故障排查。
7. 新增一个算法模块的固定动作
- 在本目录建
xxx.c/h,文件头写模块注释块(规范 V1.0),尽量零平台依赖以便免桩单测。 - 用
new-source-fileskill 注册到MDK-ARM/STM32F407-Demo.uvprojx(目录已在 IncludePath, 若新建子目录则必须补 IncludePath)。模拟器侧二选一:有消费路径 → 加simulator/CMakeLists.txt;纯算法无消费路径 → 加scripts/check_registration.py的SIM_EXEMPT(照prot_set.c/freq.c先例写理由),否则registration-check钩子拦。 - 加
test/host/<模块>/主机单测 +.pre-commit-config.yaml钩子(照phasor-unit-test抄)。 - 消费者写在
App/task/,本层只出"数";跨转换器运算的禁令见 §4。
8. 相关文档
- 通道映射权威口径:
docs/硬件平台/ADC通道映射说明.md - 分层与路线图(L1/L2 清单与状态):
docs/算法/继电保护功能复现规划.md - 傅氏算法与有效值口径:
docs/算法/傅氏算法详解.md - 采样节拍(TIM3 1 kHz + TRGO 触发 ADC1/DMA):
docs/子系统设计/ADC采样节拍设计说明.md - 相量层单测细则:
docs/测试说明/相量层单元测试说明.md