Files
STM32F4-Base/App/algo/prot_elm.c
T
wandering1 c936441c83 门禁扩容:新增 lv-conf-sync、simulator/ 纳入全部钩子;含 L3 定值/元件框架落地
本轮把"自研代码落在门禁之外"的口子收掉,同时落地 LVGL 特性裁剪与
L3 保护定值/元件框架。

门禁(.pre-commit-config.yaml,26 个钩子)
- 新增 scripts/check_lv_conf_sync.py + 钩子 lv-conf-sync:比对两份
  lv_conf.h 的**生效值**(未定义则取 lv_conf_internal.h 兜底默认),
  抓 MISMATCH / LVGL 不认的宏名 / 同文件重复定义
- 顶层 exclude 删掉 simulator/:其下 21 个 tracked 文件全是本项目自研
  代码,生成物(build/、SDL2/、tests/ref/、shot*)已在 .gitignore;
  doc-links 的 exclude 同步删除
- lv-conf-sync 的 files: 覆盖两端配置 → 只改 simulator/lv_conf.h 也触发
  (pre-commit 4.x 已移除 always: true,实测只告警并跳过,无兜底开关)
- 新增 prot-set-unit-test / prot-elm-unit-test 两个主机侧单测钩子
- S11(2.3.5) 判定细化:禁的是"用路径找文件",尖括号系统头带子目录
  (如 <sys/stat.h>)不再误报;全仓库复核仅 2 处项目代码判定变化

模拟器代码整改(纳入钩子后暴露,全部修代码而非豁免)
- cppcheck 4 处:sim_syslog.c 判空分支内解引用 p_epoch(真实 bug)、
  2 处 struct tm 未初始化、1 处指针可加 const
- coding-standard 61 处:44 处函数左大括号并到右括号同行、8 处补函数
  注释、3 处补文件头注释块、2 处 __ 前缀保护宏改 XXX_H、2 处 sprintf
  改 snprintf(写入字节数与原来完全一致)

LVGL 特性裁剪(F-1/F-2/F-3)
- 两端 lv_conf.h 对齐同一特性子集:删 26 个 LVGL 9.3 不存在的选项名、
  3 处重复定义,10 项靠默认值隐式生效的改为显式固化
- 实测对镜像体积中性;价值在防漂移,且模拟器 golden 回归跑的正是固件
  实际编译的配置

L3 软件侧
- App/algo/prot_set.* Q10 定值模型(范围校验 / 整表 all-or-nothing /
  校验和封存-自检-闭锁)+ prot_elm.* 通用"滞回+定时限"元件框架 7 实例
- test/host/prot_set、test/host/prot_elm 主机侧单测;uvprojx 挂载两处

文档同步
- pre-commit使用说明:§2.8 S11 口径、§2.15 lv-conf-sync 详解、新增 §5.5
  复盘(教训:exclude 只写真正不该进钩子的路径)
- AGENTS.md、UI_设计架构 §1.4 第 10 条、RAM 分析、审查报告刷新与销项

验证
- @build BUILD_OK 0/0(Code=194276 RO=15872 RW=876 ZI=139892)
- 模拟器重建:菜单 8 状态像素逐字节一致(池 5368 B)、11 内容页黄金参考
  全过(往返 5448 B)、remo sink 0/25600 位差异、--logtest/--cfgtest PASS
- 全仓库 cppcheck / coding-standard / no-mojibake --all-files 全过
- check_doc_links --all 129/0;check_mem_numbers --check 内存数字一致
- 端到端:往 simulator/lv_conf.h 注入一行漂移 → 钩子 FAIL(exit 1)
2026-10-11 08:48:49 +08:00

224 lines
7.5 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 <stddef.h>
/* 实例配置(ROM 常量表,数据驱动——新增元件只加一行,同 phasor k_inst 模式) */
typedef struct {
const char *name; /* 日志名 */
uint8_t over; /* 1 = 过量(越上限动作),0 = 欠量(越下限动作) */
prot_set_id_t en_id; /* 投退定值 */
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"…),run() 适配层取数用 */
} pe_cfg_t;
static const pe_cfg_t k_pe[PE_COUNT] = {
{ "OC1-IA", 1u, PS_OC1_EN, PS_OC1_CUR, PS_OC1_DLY, PS_RET_OVER, "C01" },
{ "OC2-IA", 1u, PS_OC2_EN, PS_OC2_CUR, PS_OC2_DLY, PS_RET_OVER, "C01" },
{ "OC3-IA", 1u, PS_OC3_EN, PS_OC3_CUR, PS_OC3_DLY, PS_RET_OVER, "C01" },
{ "OL-IA", 1u, PS_OL_EN, PS_OL_CUR, PS_OL_DLY, PS_RET_OVER, "C01" },
{ "OV-UA", 1u, PS_OV_EN, PS_OV_VOL, PS_OV_DLY, PS_RET_OVER, "C02" },
{ "UV-UA", 0u, PS_UV_EN, PS_UV_VOL, PS_UV_DLY, PS_RET_UNDER, "C02" },
{ "3U0-UN", 1u, PS_3U0_EN, PS_3U0_VOL, PS_3U0_DLY, PS_RET_OVER, "C03" },
};
/* 运行时状态(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];
/*
* 函数功能:按名前缀在 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;
s_rt[i].pidx = pe_find_phasor(k_pe[i].pprefix);
}
}
/*
* 函数功能:纯判据步进——按当前定值 + 给定测量值推进所有元件
* 入口参数:now_ms - 单调时基(tick ms,差值法回绕安全)
* meas - PE_COUNT 个测量值(工程量 A/V);NULL 视为全 0
* 返 回 值:无
* 函数说明:投退=0 或定值闭锁(L5-1)→ 状态整体复位、不判据;
* 滞回 = 已启动用返回门槛(定值×系数)、未启动用动作门槛;
* 延时 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;
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;
}
}
}
/*
* 函数功能:适配层——从 phasor 使用区取 rms 组装测量数组后调判据核
* 入口参数:now_ms - 单调时基(tick ms)
* 返 回 值:无
* 限定条件:仅保护消费者任务上下文;工程量↔码值换算 = 1(L1-5 标定未实现)
* 函数说明:任一相量未就绪(未满一周波)→ **整拍跳过**,不引入陈旧/半新测量。
*/
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 ((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;
}
/*
* 函数功能:取实例名(日志用)
* 入口参数: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;
}