593c5d0c84
用户裁定(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 干净。
373 lines
15 KiB
C
373 lines
15 KiB
C
/*
|
||
* 模块名称:运行参数段(全周傅氏相量/保护判据整定层,「数据可信」这一项)——实现
|
||
* 模块功能:见 run_par.h。工厂默认 = 「数据可信」下游常数按工程量重定后的额定 0.5 % 档(电压 500
|
||
* mV / 电流 5 mA / 频率 500 mHz,毫单位 int32 定点落盘);[run_par]
|
||
* 段 9 键(db_ua…db_freq,均 ≤15 字符)≤ CFG_FILE_MAX_KEYS 不拆段
|
||
* (通道系数覆盖**不落本段**——直通 adc_cal 的 [adc_cal]/[adc_cal_ofs],
|
||
* 系数唯一真源在那两段,lead 裁②a)。
|
||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||
* 作者:DTU 项目组
|
||
* 创建日期:2026-10-25(道D 运行参数段)
|
||
* 修改记录:
|
||
* v1.0 2026-10-25 DTU 项目组 创建:[run_par] 段 + 104 映射 + 直通 adc_cal
|
||
* + B7 式脏标志落盘请求
|
||
*/
|
||
|
||
/* 头文件包含区 */
|
||
#include <math.h>
|
||
#include <stddef.h>
|
||
#include <string.h>
|
||
|
||
#include "run_par.h"
|
||
|
||
#include "adc_cal.h"
|
||
#include "cfg_file.h"
|
||
|
||
#define DBG_TAG "[PAR]"
|
||
#include "dbg_log.h"
|
||
|
||
/* 常量定义区 */
|
||
|
||
/* cfg 段名(≤ CFG_FILE_MAX_NAME_LEN);键名 6~7 字符小写 */
|
||
#define RUN_PAR_SEC "run_par"
|
||
|
||
/* 104 运行参数地址(复现规划 §5.1 类内落实;权威逐项表不在仓,送检点表到位
|
||
* 后以送检为准替换本常量段——lead 裁①。0x8027~0x802A = prot_set 告警段,永不让位) */
|
||
#define RUN_PAR_IOA_DB0 0x8030u /* 9 点:0x8030~0x8038 */
|
||
#define RUN_PAR_IOA_K0 0x8040u /* k 覆盖 8 路:0x8040~0x8047 */
|
||
#define RUN_PAR_IOA_B0 0x8048u /* b 覆盖 8 路:0x8048~0x804F */
|
||
|
||
/* 死区合法域上限(毫单位):电压 50 V / 电流 1 A / 频率 5 Hz——守卫挡误写,
|
||
* 不限工况;下限统一 0(负死区 = 配置错误) */
|
||
#define RUN_PAR_DB_MAX_U 50000L
|
||
#define RUN_PAR_DB_MAX_I 1000L
|
||
#define RUN_PAR_DB_MAX_F 5000L
|
||
|
||
/* 静态变量定义区 */
|
||
|
||
/* [run_par] 段 RAM 副本(引擎原地更新;属主契约与 µs 级混窗见「数据可信」§4.4,
|
||
* 同 adc_cal——本层不挂回调,副本即生效值,消费侧实时读) */
|
||
typedef struct {
|
||
int32_t db[RUN_PAR_DB_N];
|
||
} run_par_db_t;
|
||
|
||
static const run_par_db_t s_db_rom = {
|
||
{ 500L, 500L, 500L, 500L, /* UA/UB/UC/UN:0.5 V(工程量重定后的额定档) */
|
||
5L, 5L, 5L, 5L, /* IA/IB/IC/IN:0.005 A */
|
||
500L } /* 频率:0.5 Hz(绝对档,非额定百分比) */
|
||
};
|
||
static run_par_db_t s_db;
|
||
|
||
/* 键表 offset 与 run_par_db_t 字段一一对应(sizeof(int32_t) 步进) */
|
||
static const cfg_key_def_t s_db_keys[] = {
|
||
{ "db_ua", CFG_VAL_INT, 0u, 4u },
|
||
{ "db_ub", CFG_VAL_INT, 4u, 4u },
|
||
{ "db_uc", CFG_VAL_INT, 8u, 4u },
|
||
{ "db_un", CFG_VAL_INT, 12u, 4u },
|
||
{ "db_ia", CFG_VAL_INT, 16u, 4u },
|
||
{ "db_ib", CFG_VAL_INT, 20u, 4u },
|
||
{ "db_ic", CFG_VAL_INT, 24u, 4u },
|
||
{ "db_in", CFG_VAL_INT, 28u, 4u },
|
||
{ "db_freq", CFG_VAL_INT, 32u, 4u }
|
||
};
|
||
|
||
/* 落盘脏标志:104 写成功置位,run_par_store_save 成功清零(B7 prot_set 同款,
|
||
* 只置不清 = 写盘失败天然重试);单字节,跨任务 peek 无害 */
|
||
static uint8_t s_dirty;
|
||
|
||
/* 内部函数实现区 */
|
||
|
||
/*
|
||
* 函数功能:毫单位定点 → 工程量浮点(**本模块唯一换算点**)
|
||
* 入口参数:milli - 毫单位 内 int32_t
|
||
* 返 回 值:工程量 内 float
|
||
* 限定条件:仅本文件内使用(消费侧一律走 run_par_deadband()/run_par_db_range(),
|
||
* 不许各自再写一次 /1000——那才是"第二真源"最常见的样子)
|
||
*/
|
||
static float s_milli_to_eng(int32_t milli) {
|
||
return (float)milli / 1000.0f;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:死区点量程上限(毫单位)
|
||
* 入口参数:idx - 0..8 uint8_t
|
||
* 返 回 值:上限;idx 越界返 0 内 int32_t
|
||
*/
|
||
static int32_t s_db_cap_milli(uint8_t idx) {
|
||
if (idx < 4u) {
|
||
return RUN_PAR_DB_MAX_U;
|
||
}
|
||
if (idx < RUN_PAR_DB_N - 1u) {
|
||
return RUN_PAR_DB_MAX_I;
|
||
}
|
||
if (idx == (uint8_t)(RUN_PAR_DB_N - 1u)) {
|
||
return RUN_PAR_DB_MAX_F;
|
||
}
|
||
return 0L;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:工程量浮点 → 毫单位定点(只舍不入的截断由调用侧范围守卫兜底)
|
||
* 入口参数:v - 工程量(须已判有限) float
|
||
* 返 回 值:四舍五入最近毫单位 内 int32_t
|
||
*/
|
||
static int32_t s_milli_of(float v) {
|
||
return (int32_t)lrintf(v * 1000.0f);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:死区值校验(不提交),set/104 选择段共用同一守卫 = 两路语义同源
|
||
* 入口参数:idx - 0..8 uint8_t;db - 工程量 float
|
||
* 返 回 值:合法毫单位 ≥0;-1 非法(越界/NaN/负/越程) 内 int32_t
|
||
*/
|
||
static int32_t s_db_check(uint8_t idx, float db) {
|
||
int32_t milli;
|
||
|
||
if ((idx >= RUN_PAR_DB_N) || !isfinite(db) || !(db >= 0.0f)) {
|
||
return -1L;
|
||
}
|
||
milli = s_milli_of(db);
|
||
if ((milli < 0L) || ((long long)milli > (long long)s_db_cap_milli(idx))) {
|
||
return -1L;
|
||
}
|
||
return milli;
|
||
}
|
||
|
||
/* 公共函数实现区 */
|
||
|
||
/*
|
||
* 函数功能:初始化——副本置工厂默认 + 注册 [run_par] 段(幂等,内核前)
|
||
* 返 回 值:无
|
||
*/
|
||
void run_par_init(void) {
|
||
cfg_section_reg_t reg;
|
||
|
||
(void)memcpy(&s_db, &s_db_rom, sizeof(s_db));
|
||
s_dirty = 0u;
|
||
|
||
(void)memset(®, 0, sizeof(reg));
|
||
reg.name = RUN_PAR_SEC;
|
||
reg.p_data = &s_db;
|
||
reg.p_defaults = &s_db_rom;
|
||
reg.data_size = sizeof(s_db);
|
||
reg.p_keys = s_db_keys;
|
||
reg.key_count = (uint8_t)(sizeof(s_db_keys) / sizeof(s_db_keys[0]));
|
||
reg.p_cb = NULL; /* 不挂回调:RAM 副本即生效值(同 adc_cal) */
|
||
if (cfg_file_register(®) != 0) {
|
||
DBG_ERROR("register %s FAIL (dup/full?) - file values inactive",
|
||
RUN_PAR_SEC);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:读第 idx 路死区(工程量)
|
||
* 返 回 值:V / A / Hz;越界返 0.0(消费侧按"无死区"保守处理) float
|
||
*/
|
||
float run_par_deadband(uint8_t idx) {
|
||
if (idx >= RUN_PAR_DB_N) {
|
||
return 0.0f;
|
||
}
|
||
return s_milli_to_eng(s_db.db[idx]); /* 毫→工程:唯一换算点 = s_milli_to_eng */
|
||
}
|
||
|
||
/*
|
||
* 函数功能:取第 idx 路死区的合法域(工程量)——**合法域的唯一出口**
|
||
* 入口参数:idx - 0..8 uint8_t;lo/hi - 输出下限/上限(工程量 V/A/Hz) float *
|
||
* 返回值:0 - 已写出;-1 - idx 越界或指针为空(调用方不得回退成自造默认域) int
|
||
* 限定条件:任意上下文(纯常量读,无锁、无 I/O)
|
||
* 函数说明:为什么公开这个口:UI 整定页要给数字编辑器设 min/max,而这些上限的
|
||
* 真源是本模块的 RUN_PAR_DB_MAX_*(电压 50V / 电流 1A / 频率 5Hz,
|
||
* "额定 × 二档裕量")。页面里抄一份就是第二真源——判据「不新增第二份
|
||
* 真源」最典型的复发形态是"顺手把常量写进界面代码"。因此本函数的
|
||
* 消费者是装配层 App/app/ui_data_feed.c 的注入表实现,不是页面本身。
|
||
* ⚠ 给的是**粗守域**(挡误写),不是工况域;真提交仍由 s_db_check
|
||
* 同源守卫裁(104 选择段与 UI 编辑器共用同一批常量,不会漂移)。
|
||
*/
|
||
int run_par_db_range(uint8_t idx, float *lo, float *hi) {
|
||
if ((idx >= RUN_PAR_DB_N) || (lo == NULL) || (hi == NULL)) {
|
||
return -1;
|
||
}
|
||
*lo = 0.0f;
|
||
*hi = s_milli_to_eng(s_db_cap_milli(idx));
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:写第 idx 路死区(守卫通过才提交;成功置脏)
|
||
* 返 回 值:0 成功;-1 非法(副本不动) int
|
||
*/
|
||
int run_par_deadband_set(uint8_t idx, float db) {
|
||
int32_t milli = s_db_check(idx, db);
|
||
|
||
if (milli < 0L) {
|
||
return -1;
|
||
}
|
||
s_db.db[idx] = milli;
|
||
s_dirty = 1u;
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:104 运行参数地址读(命中集精确匹配,见 run_par.h 地址表)
|
||
* 返 回 值:0 命中;-1 非本段(调用方落 prot_set) int
|
||
*/
|
||
int run_par_104_read(uint32_t addr, float *out) {
|
||
if (out == NULL) {
|
||
return -1;
|
||
}
|
||
if ((addr >= RUN_PAR_IOA_DB0) && (addr < RUN_PAR_IOA_DB0 + RUN_PAR_DB_N)) {
|
||
*out = run_par_deadband((uint8_t)(addr - RUN_PAR_IOA_DB0));
|
||
return 0;
|
||
}
|
||
if ((addr >= RUN_PAR_IOA_K0) && (addr < RUN_PAR_IOA_K0 + RUN_PAR_CH_N)) {
|
||
*out = adc_cal_k_uv((adc_semantic_t)(addr - RUN_PAR_IOA_K0));
|
||
return 0;
|
||
}
|
||
if ((addr >= RUN_PAR_IOA_B0) && (addr < RUN_PAR_IOA_B0 + RUN_PAR_CH_N)) {
|
||
*out = adc_cal_b_codes((adc_semantic_t)(addr - RUN_PAR_IOA_B0));
|
||
return 0;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:写某路系数 k(µV/码)**并在同一函数内置脏**——整定的语义写入口
|
||
* 入口参数:sem - 电气语义通道 adc_semantic_t;k_uv - 系数(µV/码,>0) float
|
||
* 返回值:0 - RAM 副本已写 + 已置脏;-1 - 粗守或执行段拒(副本未动、不置脏) int
|
||
* 限定条件:整定路径上下文(iec104Task 的 104 执行段;UI 经 ui_data_yt_ops_t 由
|
||
* 装配层 App/app/ui_data_feed.c 转进来)。勿在 adcTask 调用
|
||
* 函数说明:**为什么"写值 + 置脏"必须长在一个函数里**(lead 裁②,2026-10-27):
|
||
* 置脏是持久化承诺,写值是 RAM 事实,两者拆开就会出现"屏上已改、盘上
|
||
* 永不落、且没有任何错误返回"这种无人报错的状态——本仓今天已实测到它的
|
||
* 既有形态(绕过本模块直调 adc_cal_set() 的写者不置脏,值永远不落盘,
|
||
* 另立 task-43 处置)。所以这里不提供单独的 mark_dirty 公开口:它一旦
|
||
* 存在就一定有人只调它(同"零消费者的状态查询口"那条教训)。
|
||
* 零点 b 走 RMW 取**当前生效值**(经合法带哨兵,天然满足再写校验)。
|
||
* 粗守(0 < k ≤ RUN_PAR_K_MAX_UV)只为挡误写,真带在 adc_cal 执行段裁:
|
||
* 两层各说各的,页面上"合法域内仍被拒(ERR)"就是执行段拒的,不伪装。
|
||
*/
|
||
int run_par_cal_k_set(adc_semantic_t sem, float k_uv) {
|
||
int32_t k_q16;
|
||
int32_t b_codes;
|
||
|
||
/* 通道号不在这里判:adc_cal_set 自带合法性与合法带裁决(越界一律 -1),
|
||
* 本层再判一次就是第二真源。粗守只挡误写:0 < k ≤ RUN_PAR_K_MAX_UV */
|
||
if (!isfinite(k_uv) || !(k_uv > 0.0f) || (k_uv > RUN_PAR_K_MAX_UV)) {
|
||
return -1;
|
||
}
|
||
k_q16 = (int32_t)lrintf(k_uv * 65536.0f);
|
||
b_codes = (int32_t)lrintf(adc_cal_b_codes(sem));
|
||
if (adc_cal_set(sem, k_q16, b_codes) != 0) {
|
||
return -1; /* 执行段拒:副本未动,脏位也不置 = 不承诺任何持久化 */
|
||
}
|
||
s_dirty = 1u;
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:写某路零点偏移 b(码值)并置脏(与 k 同款语义,粗守 ±1e6 码)
|
||
* 入口参数:sem - 电气语义通道 adc_semantic_t;b_codes - 零点(码值) int32_t
|
||
* 返回值:0 - 已写并置脏;-1 - 粗守或执行段拒(副本未动、不置脏) int
|
||
* 限定条件:同 run_par_cal_k_set()
|
||
* 函数说明:k 侧 RMW 用当前生效值;±8192 真带由 adc_cal 执行段裁(粗守目的是
|
||
* 挡误写不是限工况)。本函数的消费者 = 104 的 0x8048~ 分支(改造后),
|
||
* 不是"为将来预留"的口
|
||
*/
|
||
int run_par_cal_b_set(adc_semantic_t sem, int32_t b_codes) {
|
||
int32_t k_q16;
|
||
|
||
if (((float)b_codes < -1.0e6f) || ((float)b_codes > 1.0e6f)) {
|
||
return -1;
|
||
}
|
||
k_q16 = (int32_t)lrintf(adc_cal_k_uv(sem) * 65536.0f);
|
||
if (adc_cal_set(sem, k_q16, b_codes) != 0) {
|
||
return -1;
|
||
}
|
||
s_dirty = 1u;
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:104 运行参数地址写(两段式;k/b 经本模块语义 setter——**run_par 不存
|
||
* 系数**,系数唯一真源 = [adc_cal]/[adc_cal_ofs],lead 裁②a)
|
||
* 返 回 值:0 接受 / -1 拒绝 / -2 非本段(调用方落 prot_set) int
|
||
* 函数说明:选择段 = 校验不提交(死区走与 set 同源守卫;k/b 的 adc_cal 合法带
|
||
* 由执行段裁决——IEC104 允许执行段 PN,两段结果可不一致,注释在此
|
||
* 明说防"选择过了怎么还 PN"的现场困惑)。k/b 两支自 2026-10-27 起一律
|
||
* 走 run_par_cal_k_set() / run_par_cal_b_set():写值与置脏在同一个函数
|
||
* 里发生,本函数不再自己碰 s_dirty(lead 裁②:消除两份置脏逻辑)。
|
||
*/
|
||
int run_par_104_write(uint32_t addr, float v, uint8_t select) {
|
||
uint8_t slot; /* 每个命中分支先赋值再使用(无未init读路径) */
|
||
|
||
if ((addr >= RUN_PAR_IOA_DB0) && (addr < RUN_PAR_IOA_DB0 + RUN_PAR_DB_N)) {
|
||
int32_t milli;
|
||
|
||
slot = (uint8_t)(addr - RUN_PAR_IOA_DB0);
|
||
milli = s_db_check(slot, v);
|
||
if (milli < 0L) {
|
||
return -1;
|
||
}
|
||
if (select == 0u) {
|
||
s_db.db[slot] = milli;
|
||
s_dirty = 1u;
|
||
}
|
||
return 0;
|
||
}
|
||
if ((addr >= RUN_PAR_IOA_K0) && (addr < RUN_PAR_IOA_K0 + RUN_PAR_CH_N)) {
|
||
slot = (uint8_t)(addr - RUN_PAR_IOA_K0);
|
||
if (!isfinite(v) || !(v > 0.0f) || (v > 1.0e9f)) {
|
||
return -1;
|
||
}
|
||
if (select != 0u) {
|
||
return 0; /* 粗守过了 ≠ 执行必过:合法带裁决在执行段(见函数头) */
|
||
}
|
||
/* 写值 + 置脏收进语义 setter(粗守 0<k≤上限 也在里面,与本支
|
||
* 原先那条逐字同值守卫一致,行为不变)*/
|
||
return run_par_cal_k_set((adc_semantic_t)slot, v);
|
||
}
|
||
if ((addr >= RUN_PAR_IOA_B0) && (addr < RUN_PAR_IOA_B0 + RUN_PAR_CH_N)) {
|
||
slot = (uint8_t)(addr - RUN_PAR_IOA_B0);
|
||
if (!isfinite(v)) {
|
||
return -1; /* NaN/Inf 不得进 lrintf(该转换是 UB) */
|
||
}
|
||
if (select != 0u) {
|
||
return 0;
|
||
}
|
||
/* 同款语义 setter:粗守 ±1e6 码在 setter 内,±8192 真带由 adc_cal 执行段裁 */
|
||
return run_par_cal_b_set((adc_semantic_t)slot, (int32_t)lrintf(v));
|
||
}
|
||
return -2;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:落盘脏探查(非消费)
|
||
* 返 回 值:1 有未落盘整定;0 干净 uint8_t
|
||
*/
|
||
uint8_t run_par_dirty_peek(void) {
|
||
return s_dirty;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:执行落盘(整档外科保存成功后清脏;失败脏保持 = 下轮重试)
|
||
* 返 回 值:0 成功/无脏;-1 写盘失败 int
|
||
* 限定条件:defaultTask(配置引擎属主)上下文,SYS_EVT_RUN_PAR_SAVE 驱动
|
||
* 函数说明:一次 save 同时覆盖 [run_par] 9 键与刚被 adc_cal_set 改写的
|
||
* [adc_cal]/[adc_cal_ofs] 副本(lead 裁②:一条事件位一次 save)。
|
||
* 成功路径上给标定层盖一次落盘水位(adc_cal_note_persisted())——它是
|
||
* **观测回执**,不参与本函数任何判定;失败路径不盖,于是"save 一直失败"
|
||
* 在标定层侧表现为差额长期不归零(道G task-43 第②项)。
|
||
*/
|
||
int run_par_store_save(void) {
|
||
if (s_dirty == 0u) {
|
||
return 0;
|
||
}
|
||
if (cfg_file_save() != 0) {
|
||
DBG_ERROR("cfg save FAIL - keep dirty, retry next round");
|
||
return -1;
|
||
}
|
||
s_dirty = 0u;
|
||
adc_cal_note_persisted(); /* 观测回执:水位追平,差额归零(不是判定输入) */
|
||
return 0;
|
||
}
|