Files
STM32F4-Base/App/algo/prot_elm.c
T
wandering1 94b0d79026 feat(ui)+chore(docs): UI 补齐清单清空;数据可信销项与「卡」号清退
本笔含两组收尾工作(同一工作区内交错在 6 个共享文档/门禁配置里,
拆开会产生"文档声称的页数与树里黄金数不一致"的中间提交,故合为一笔):

一、UI 补齐(清单清空)
- 新建 6 个内容页模块:ui_page_accident / oper / alarm / start / yk
  复用 ui_w_records 共享组件(分页、空态"无记录"、按实例读盘缓存),
  记录层与五个写入点已由道F task-10 / 道G task-12 落地,本批只补页面;
  ui_page_runreport 为 13 行 2 页只读统计,四个数逐个挂范围标签
  (自电池周期 / 本次开机),boot_ready=0 显 "--" 不显 0。
- 数据面:ui_data_info_t 尾部追加 9 字段,ui_data_feed 用
  boot_stats_get() + iec104_serv_get_stats() 纯搬运;模拟器填确定值假数据
  (差异登记 docs/测试说明/模拟器差距清单.md §2.3 两行)。
- 接线:菜单树 6 叶绑 #page=,绑定叶 22→28、UI_PAGE_MAP_COUNT=28;
  字库 185→218 字 / 280→313 字形;双端注册 uvprojx +30、CMakeLists +6。
- 回归:内容页黄金 53→60(含 runreport_p2 真拍第 2 页),文本断言
  425 正向 / 36 反向;存量 53 张逐字节零漂移;记录 5 页各用独立空目录
  (模拟器把操作留痕写进 cwd 的 sim_oper.log,共用 tmp 会让空态拍不到)。

二、数据可信销项 + 「数据可信」任务卡号整族清退
- 删除已闭环的"已知电压注入验证"行与闭环结论块(证据在
  docs/硬件平台/ADC通道映射说明.md §8.1 与
  docs/测试说明/ADC前端注入验证流程.md §5~§7),§5 标识列改语义短语。
- 卡 0/0b/1~12 共 14 个号全仓 288 站点 / 56 文件清退成语义短语,
  代码注释、测试、门禁配置与文档一并改;换算表入 docs/编号规范.md §9.3。
  这一族是中文前缀,不进 check_ids 语料,刻意不入 id_aliases.json。

验证(实测)
- @build 0 错 0 警:Code=236456 RO=28124 RW=1876 ZI=137084;
  RAM +464 B(RW +304 / ZI +160),RW_IRAM1 62.2% / RW_CCM 87.9%,
  体积同码凭证与 RAM 台账已回填。
- check_menu_rows 8 状态 rc=0(池 5368/5448 常量未动);
  check_content_pages 60 页 rc=0;check_font_charset / registration /
  coding-standard / bom-drift / mojibake / doc-links --all / layer-deps /
  nested-comment 全 rc=0;check_ids --strict 无错误;主机单测
  ui_data 295 / boot_stats 21 / sys_log_mcu 141 / soe 32 / soe_store 27。

未做(不是遗漏)
- 六页真机判据(真记录翻页、掉电保持、清除后归零、boot_count 自增、
  拔 RTC 电池落 RESTARTED、IWDG 复位落 rst_cause、主站连一次 conn_up+1)
  已登记 docs/任务规划/登记簿.md §7;模拟器结构上照不到。
- docs/产品需求/产品需求设计文档.md 属另一条在飞改动,本笔不含;
  其中我已同步 6 处现状数字(28 叶 / 60 页 / 218 字),随该笔入库。
2026-10-28 07:23:16 +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.
/*
* 模块名称:保护元件框架(L3,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)
* 返 回 值:无
* 限定条件:仅保护消费者任务上下文;测量已是 L1.5 工程量(未标定域由
* 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(供 L4 出口 / 心跳日志)
*/
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;
}