/* * 模块名称:运行参数段(L2/L3 整定层,「数据可信」卡 10)——实现 * 模块功能:见 run_par.h。工厂默认 = 「数据可信」卡 6 额定 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 卡 10) * 修改记录: * v1.0 2026-10-25 DTU 项目组 创建:[run_par] 段 + 104 映射 + 直通 adc_cal * + B7 式脏标志落盘请求 */ /* 头文件包含区 */ #include #include #include #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(卡 6 额定档) */ 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= 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; }