Files
STM32F4-Base/App/algo/prot_elm.c
T
wandering1 593c5d0c84 refactor(编号): 采集链分层 L1/L1.5/L2/L2.5/L3/L4/L5 → 语义名(批次 1)
用户裁定(2026-10-28):L1/L1.5/L2/L2.5/L3 这套层号「谁能看懂」,改用语义名。
映射(同一批定稿,含 L4/L5):
  L1   → 语义取数与有效性
  L1.5 → 标定折算
  L2   → 全周傅氏相量
  L2.5 → 测频与遥测快照
  L3   → 保护判据
  L4   → 出口与记录
  L5   → 动作留痕

实测规模:384 站点 / 62 文件(App/algo、App/task、test/host 六个套件、docs/算法、
产品需求、架构总览、启动顺序、ADC 采样节拍、IEC104 上行、采集短板评测、内存管理、
AGENTS.md、README.md、docs/README.md、.pre-commit-config.yaml 与 scripts 里的注释)。

改法(不是全局替换,逐模式有上下文规则):
- 先吃同位语再落裸词:L1 有效性校验 → 语义取数与有效性校验、L2 相量层 → 全周傅氏
  相量层、L3 保护 → 保护判据、L2.5 遥测快照 → 测频与遥测快照,避免「保护判据保护判据」
  这类重复(第一版就是这么坏掉的,已整体回滚重做)。
- .c/.h 只改注释:先把字符串字面量切出去(引号数为奇数的跨行注释按整行处理),
  C 字符串里的 L2-in ring 之类原样保留——AGENTS 铁律「字符串字面量必须纯 ASCII」。
- 层内条目号(L1-3 / L2-3 这类)**本批不动**,属批次 2;另外三族(UI 菜单层级
  L0~L3、链路延迟分段 L1~L8、CubeMX 防丢失三层)也不动,属批次 3。
- 顺手修的措辞:范围写法 L1~L3 → 「分层(语义取数与有效性 → 保护判据)」、
  CI 段「不含 L1/L2/L3」→「不含采集链分层」、目录地图里同一处并列表述。

验证:@build 0 错 0 警(Code=236456,与改前逐字节同尺寸——纯注释改动);
内存布局配图已按新脚本重生成(自检通过);doc-links --all --sections-strict 通过
(195 文档);mojibake / bom-guard / coding-standard / 新编号门禁自查 全 rc=0;
git diff --check 干净。
2026-10-28 13:04:02 +08:00

332 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:保护元件框架(保护判据,P1 子集)——实现
* 模块功能:见 prot_elm.h。判据核 prot_elm_eval() 为纯逻辑(prot_set RAM +
* 测量数组 → 状态机),prot_elm_run() 是 phasor→测量的薄适配层。
*
* 创建日期:2026-10-11
*/
#include "prot_elm.h"
#include "prot_set.h"
#include "phasor.h"
#include "freq.h"
#include <stddef.h>
/* 实例配置(ROM 常量表,数据驱动——新增元件只加一行,同 phasor k_inst 模式) */
typedef struct {
const char *name; /* 日志名 */
uint8_t over; /* 1 = 过量(越上限动作),0 = 欠量(越下限动作) */
uint8_t src; /* 测量源:0 = phasor 基波有效值,1 = freq 测频 */
prot_set_id_t en_id; /* 投退定值 */
uint8_t trip_ctl; /* 跳闸投退定值项(PS_*_TRIP);PROT_ELM_NO_TRIP =
* 该元件没有这一项(告警类)→ 动作只出告警记录。
* 用 uint8_t 而不是 prot_set_id_t 就是因为要能表达
* "无此项"这个第 41 种取值(PS_ID_COUNT 是数组长度,
* 拿去喂 prot_set_get_ctl() 会越界读) */
prot_set_id_t set_id; /* 动作定值 */
prot_set_id_t dly_id; /* 延时定值(ms) */
prot_set_id_t ret_id; /* 返回系数定值(RET_OVER / RET_UNDER) */
const char *pprefix; /* 相量实例名前缀("C01"…);src=1 时为 0(不用) */
} pe_cfg_t;
/* k_pe[].trip_ctl 的"无此定值项"取值(见上方字段注释) */
#define PROT_ELM_NO_TRIP 0xFFu
/* 测量源枚举(pe_cfg_t.src) */
#define PE_SRC_PHASOR 0u
#define PE_SRC_FREQ 1u
/* freq 无有效读数时给欠量元件的"必不启动"填充值(远大于任何欠量定值上限) */
#define PE_MEAS_BIG 1e30f
/* 列序 = pe_cfg_t 字段序(name, over, src, en_id, trip_ctl, set_id, dly_id, ret_id,
* pprefix)。⚠ trip_ctl 紧跟 en_id 插入,新增元件必须照这一列的口径填:有跳闸投退
* 定值项的填 PS_*_TRIP,没有的填 PROT_ELM_NO_TRIP(= 该元件动作只出告警记录)。*/
static const pe_cfg_t k_pe[PE_COUNT] = { /* ← 表头注释见上,行内列序与之一致 */
{ "OC1-IA", 1u, PE_SRC_PHASOR, PS_OC1_EN, PS_OC1_TRIP, PS_OC1_CUR, PS_OC1_DLY, PS_RET_OVER, "C01" },
{ "OC2-IA", 1u, PE_SRC_PHASOR, PS_OC2_EN, PS_OC2_TRIP, PS_OC2_CUR, PS_OC2_DLY, PS_RET_OVER, "C01" },
{ "OC3-IA", 1u, PE_SRC_PHASOR, PS_OC3_EN, PS_OC3_TRIP, PS_OC3_CUR, PS_OC3_DLY, PS_RET_OVER, "C01" },
{ "OL-IA", 1u, PE_SRC_PHASOR, PS_OL_EN, PS_OL_TRIP, PS_OL_CUR, PS_OL_DLY, PS_RET_OVER, "C01" },
/* ↓ 三项 + 下面两项频率元件:参考系统的告警段(0x8027~0x802A)本就没有「跳闸
投退」这一档,g_prot_set_meta 也无对应项 → PROT_ELM_NO_TRIP,动作一律归告警。
这不是兜底猜测,是照实反映("哪些元件算跳闸"的真源始终是这张表 + 定值表)。*/
{ "OV-UA", 1u, PE_SRC_PHASOR, PS_OV_EN, PROT_ELM_NO_TRIP, PS_OV_VOL, PS_OV_DLY, PS_RET_OVER, "C02" },
{ "UV-UA", 0u, PE_SRC_PHASOR, PS_UV_EN, PROT_ELM_NO_TRIP, PS_UV_VOL, PS_UV_DLY, PS_RET_UNDER, "C02" },
{ "3U0-UN", 1u, PE_SRC_PHASOR, PS_3U0_EN, PROT_ELM_NO_TRIP, PS_3U0_VOL, PS_3U0_DLY, PS_RET_OVER, "C03" },
/* 过频/低频(过频 / 低频,B8):测量量 = freq 层测得频率(Hz),返回系数独立
* (RET_OF/RET_UF——频率滞回带与电气量不同量级,勿共用 0.95/1.05) */
{ "OF-F", 1u, PE_SRC_FREQ, PS_OF_EN, PROT_ELM_NO_TRIP, PS_OF_HZ, PS_OF_DLY, PS_RET_OF, 0 },
{ "UF-F", 0u, PE_SRC_FREQ, PS_UF_EN, PROT_ELM_NO_TRIP, PS_UF_HZ, PS_UF_DLY, PS_RET_UF, 0 },
};
/* 运行时状态(picked/asserted 的对外快照 + 延时起点 + 相量实例索引缓存) */
typedef struct {
uint8_t enabled;
uint8_t picked;
uint8_t asserted;
uint32_t t0; /* picked 起始时基(picked=0 时无意义) */
uint8_t pidx; /* 相量实例索引;0xFF = 未解析/未找到 */
} pe_rt_t;
static pe_rt_t s_rt[PE_COUNT];
/* 跨上下文复归请求(单字标志,volatile;B5:104 YK11 在 iec104Task 落请求) */
static volatile uint8_t s_reset_req;
/*
* 函数功能:按名前缀在 phasor 实例表里找索引(开机解析一次)
* 入口参数:prefix - 相量实例名前缀(如 "C01")
* 返 回 值:实例索引;0xFF = 未找到(run() 对该元件整拍跳过)
*/
static uint8_t pe_find_phasor(const char *prefix) {
uint8_t i, n = phasor_inst_count();
for (i = 0u; i < n; i++) {
const phasor_inst_t *pi = phasor_inst(i);
uint8_t j = 0u, hit = 1u;
if (pi == 0) {
continue;
}
while ((prefix[j] != '\0') && (pi->name[j] != '\0')) {
if (prefix[j] != pi->name[j]) {
hit = 0u;
break;
}
j++;
}
if ((hit != 0u) && (prefix[j] == '\0')) {
return i;
}
}
return 0xFFu;
}
/*
* 函数功能:复位全部元件状态并解析相量索引(定值不动;幂等)
* 入口参数:无
* 返 回 值:无
* 限定条件:须在 phasor_init() 之后调用(否则实例表名查不到 → 全 0xFF)
*/
void prot_elm_init(void) {
uint32_t i;
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
s_rt[i].enabled = 0u;
s_rt[i].picked = 0u;
s_rt[i].asserted = 0u;
s_rt[i].t0 = 0u;
/* src=FREQ 的行无相量绑定(pprefix=0)→ pidx 置未解析(run 不查它) */
s_rt[i].pidx = (k_pe[i].pprefix != 0) ? pe_find_phasor(k_pe[i].pprefix) : 0xFFu;
}
}
/*
* 函数功能:登记"复归元件状态"请求(任意上下文安全,见 prot_elm.h)
* 入口参数:无
* 返 回 值:无
* 函数说明:只置单字标志;真正的状态复位在下一拍 eval 头部(消费者上下文)执行。
*/
void prot_elm_request_reset(void) {
s_reset_req = 1u;
}
/*
* 函数功能:纯判据步进——按当前定值 + 给定测量值推进所有元件
* 入口参数:now_ms - 单调时基(tick ms,差值法回绕安全)
* meas - PE_COUNT 个测量值(工程量 A/V);NULL 视为全 0
* 返 回 值:无
* 函数说明:投退=0 或定值闭锁(定值校验和)→ 状态整体复位、不判据;
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
* 滞回 = 已启动用返回门槛(定值×系数)、未启动用动作门槛;
* 延时 dly=0 → 启动同拍即动作;返回即时(P4 接出口时再补返回延时)。
*/
void prot_elm_eval(uint32_t now_ms, const float meas[PE_COUNT]) {
uint8_t locked = prot_set_locked();
uint32_t i;
/* 跨上下文复归请求在此消费(YK11 经 request_reset,见 prot_elm.h) */
if (s_reset_req != 0u) {
s_reset_req = 0u;
prot_elm_init();
locked = prot_set_locked();
}
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
const pe_cfg_t *cfg = &k_pe[i];
uint8_t cond;
float m, set, ret;
s_rt[i].enabled = ((locked == 0u) && (prot_set_get_ctl(cfg->en_id) != 0u)) ? 1u : 0u;
if (s_rt[i].enabled == 0u) {
/* 退出/闭锁:状态整体复位(参考元件闭锁分支的 Rly/Timer 复位) */
s_rt[i].picked = 0u;
s_rt[i].asserted = 0u;
s_rt[i].t0 = 0u;
continue;
}
m = (meas != 0) ? meas[i] : 0.0f;
set = prot_set_get_f(cfg->set_id);
ret = set * prot_set_get_f(cfg->ret_id);
/* 滞回:已启动用返回门槛,未启动用动作门槛(参考 M_GRly_OverRelay) */
if (cfg->over != 0u) {
cond = (s_rt[i].picked != 0u) ? (uint8_t)(m > ret) : (uint8_t)(m > set);
} else {
cond = (s_rt[i].picked != 0u) ? (uint8_t)(m < ret) : (uint8_t)(m < set);
}
if (cond != 0u) {
float dly;
if (s_rt[i].picked == 0u) {
s_rt[i].picked = 1u;
s_rt[i].t0 = now_ms;
}
dly = prot_set_get_f(cfg->dly_id);
if (dly < 0.0f) {
dly = 0.0f;
}
/* 差值法比较,tick 回绕安全;dly=0 → 启动同拍即动作 */
if ((uint32_t)(int32_t)(now_ms - s_rt[i].t0) >= (uint32_t)dly) {
s_rt[i].asserted = 1u;
}
} else {
s_rt[i].picked = 0u;
s_rt[i].asserted = 0u;
s_rt[i].t0 = 0u;
}
}
}
/*
* 函数功能:适配层——按实例表 src 组装测量数组后调判据核
* 入口参数:now_ms - 单调时基(tick ms)
* 返 回 值:无
* 限定条件:仅保护消费者任务上下文;测量已是 标定折算工程量(未标定域由
* protectTask 未标定即闭锁挡在门外,本函数不感知标定态)
* 函数说明:src=PHASOR 行任一相量未就绪(未满一周波)→ **整拍跳过**,
* 不引入陈旧/半新测量(B5 口径)。src=FREQ 行:freq 有有效读数
* (started 且幅值门 OK)→ 填测得频率;否则按方向填"必不启动"值
* (过量 0 / 欠量 PE_MEAS_BIG → 滞回比较恒假 → 元件复位),
* 不整拍跳过——频率源缺失不得饿死电流/电压类元件。
*/
void prot_elm_run(uint32_t now_ms) {
float meas[PE_COUNT];
phasor_t p;
uint32_t i;
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
if (k_pe[i].src == PE_SRC_FREQ) {
float f;
if ((freq_get(&f) == 0) && (freq_valid() != 0u)) {
meas[i] = f;
} else {
meas[i] = (k_pe[i].over != 0u) ? 0.0f : PE_MEAS_BIG;
}
continue;
}
if ((s_rt[i].pidx == 0xFFu) || (phasor_get(s_rt[i].pidx, &p) != 0)) {
return;
}
meas[i] = p.fAmSQ;
}
prot_elm_eval(now_ms, meas);
}
/*
* 函数功能:读单元件状态快照(拷贝语义)
* 入口参数:id - 元件编号;out - 快照输出(NULL 直接返回)
* 返 回 值:无;越界编号输出全零
*/
void prot_elm_state(prot_elm_id_t id, prot_elm_state_t *out) {
if (out == 0) {
return;
}
out->enabled = 0u;
out->picked = 0u;
out->asserted = 0u;
out->pick_ms = 0u;
if (id < PE_COUNT) {
out->enabled = s_rt[(uint32_t)id].enabled;
out->picked = s_rt[(uint32_t)id].picked;
out->asserted = s_rt[(uint32_t)id].asserted;
out->pick_ms = s_rt[(uint32_t)id].t0;
}
}
/*
* 函数功能:聚合动作位图
* 入口参数:无
* 返 回 值:bit i = 元件 i asserted(供 出口与记录 / 心跳日志)
*/
uint32_t prot_elm_active_mask(void) {
uint32_t mask = 0u;
uint32_t i;
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
if (s_rt[i].asserted != 0u) {
mask |= (1uL << i);
}
}
return mask;
}
/*
* 函数功能:聚合启动位图
* 入口参数:无
* 返 回 值:bit i = 元件 i picked(越限电平,早于/等于 asserted)
* 限定条件:任意上下文(只读快照,无锁;与 prot_elm_active_mask 同口径)
* 函数说明:调用方是 protectTask 的「保护启动」记录边沿检测(task-10)。本函数
* **只聚合电平**,不造边沿——边沿属观测者状态(s_prev_pick 在那一侧),
* 判据核里再存一份"上一拍"会让 eval 不再幂等。
*/
uint32_t prot_elm_pick_mask(void) {
uint32_t mask = 0u;
uint32_t i;
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
if (s_rt[i].picked != 0u) {
mask |= (1uL << i);
}
}
return mask;
}
/*
* 函数功能:判定该元件动作上升沿应归哪一类记录(见 prot_elm.h 同号声明)
* 入口参数:id - 元件编号 prot_elm_id_t
* 返 回 值:PROT_REC_TRIP / PROT_REC_ALARM / PROT_REC_NONE(id 越界) prot_rec_kind_t
* 限定条件:任意上下文;只读,不改任何状态
* 函数说明:判据唯一来源 = k_pe[id].trip_ctl 指向的那枚 PS_*_TRIP 投退定值。
* trip_ctl == PROT_ELM_NO_TRIP 时**先返回告警**,绝不把 0xFF 喂给
* prot_set_get_ctl()(那是数组越界读,会读到定值区外的 RAM——
* 与"未标定拿 ADC 引脚电压当二次值"同类的静默错数据形状)。
*/
prot_rec_kind_t prot_elm_record_kind(prot_elm_id_t id) {
uint8_t trip_ctl;
if (id >= PE_COUNT) {
return PROT_REC_NONE;
}
trip_ctl = k_pe[(uint32_t)id].trip_ctl;
if (trip_ctl == (uint8_t)PROT_ELM_NO_TRIP) {
return PROT_REC_ALARM;
}
return (prot_set_get_ctl((prot_set_id_t)trip_ctl) != 0u) ? PROT_REC_TRIP : PROT_REC_ALARM;
}
/*
* 函数功能:取实例名(日志用)
* 入口参数:id - 元件编号
* 返 回 值:ROM 常量名;越界编号返回 "PE??"
*/
const char *prot_elm_name(prot_elm_id_t id) {
if (id >= PE_COUNT) {
return "PE??";
}
return k_pe[(uint32_t)id].name;
}