Files
STM32F4-Base/App/algo/run_par.c
T
wandering1 728d60675f feat(UI/采集/104): 通讯设置页 + 开机账本 + adc_cal 可诊断化——三车道同树一次构建入库
三条道在同一棵树上交付,故合并成**一次权威构建**(@build 0 错 0 警,Code=235236 RO=26308 RW=1572
ZI=136924)后同笔入库;台账已按该构建回填(`ram_baseline.py --update` + `--write`、`chec
k_mem_numbers.py
 --write`),凭证与实际 .map 同源。归属如下:

· **通讯设置页**(道G task-9):新增 `Drivers/BSP/UI/ui_page_comm.{c,h}`(通道六注入,与死
区页同骨架同纪
  律),`#page=comm` 绑定使 bound **21 → 22**、内容页黄金 **47 → 53**(旧 47 页字节零变化,
新增六态含
  WAIT/OK 两张);`iec104_comm` 脏位 + store_save(**无事件位**)挂 defaultTask 同点位
轮询;`port_104` 由
  宏改走 getter、`IEC104_SERV_PORT` 删除;字体 +听/监(185 字全覆)。**实机链已闭环**:FTP 外改
[comm]
  的 ca/port → 复位 → 串口出现 `section 'comm' (2 keys)` 与 `i104: listening o
n 9004` → 还原 2404,全程无
  FAIL ⇒「重启即生效 + 端口真值」不是推断是实测。上限核对 6/8 段、2/16 键,超限现象由 TC-CONFIG-056/057
  钉死(注册 -1 + `comm init: FAIL` 锚 + getter 回 ROM),**不抬宏**。
· **开机账本**(道F task-12):新增 `App/util/boot_stats.{c,h}`——上电次数落 SD2506 用户
 SRAM(magic +
  小端计数 + 版本 + 异或校验),一次上电 = 1 读 + ≤1 写,**零 NAND、零卷锁、零配置段**;四态互斥
  UNKNOWN / FRESH / CONTINUED / RESTARTED(BLF=1 才算电池域掉过电),**读失败绝不回写**;
文案口径「自 RTC
  电池周期起」,**禁写终身次数**。复位原因优先级链从 `app_main.c` 的 if-else 收进一份真源(串口与屏幕同
  源);104 四个只增不清计数 + `iec104_serv_get_stats()` 留在 serv、未塞进 link(会被 open
/close 洗掉)。
  运行时长按用户裁定 = **只报本次开机以来**,零新增代码。新套件 `test/host/boot_stats` 21/21,五处注入
  **全部有牙**(含「漂一字节被校验和挡下而不是报 256 倍错账」)。
· **adc_cal 可诊断化**(道E task-43):三个 volatile 计数 + `adc_cal_get_stats()`
/ `adc_cal_note_persisted()`
  (水位**盖写非自增**,因为一次 save 覆盖整档,数 save 会留假未落盘);「差额 × 脏位」三态语义唯一落在
  `adc_cal.h`,其中「差额 > 0 且脏位恒 0」= 绕过语义 setter 的静默丢失;TC-ADCCAL-019~023。

三条**必须留档的自纠**(都是执行道自己报的,我不替他们润色):
① stdint 那条断链是**宿主套件抓到、通讯道先修**,采集道承认自己是重复添加者并删掉自己那份;
② 「exit 2 不是证据,断言变红才是」——采集道第一版反证注入了未声明标识符导致编译错,差点当成功;
③ 套件汇总行原写 `s_total/s_total`,失败时会打印「21/21 PASSED (failed=1)」这种自相矛盾的行,已改。

另修:`check_registration.py` 的 `SIM_EXEMPT` 加一行豁免 `App/util/boot_stats.c
`(依赖 I2C 与 RCC 位掩码,
MCU 专属;核过 `sys_clock.c`/`sha256.c` 同形态先例),注册反向校验 exit 1 → 0。发号 TC-IEC1
04-034、
TC-UIDATA-029 已实抬游标(`TC-IEC104-033→034`、`TC-UIDATA-026→029`)。

门禁:id-check --strict 与 plain 均 rc=0、doc-links --all rc=0、注册校验 0、cppche
ck / coding-standard /
bom-drift 0;iec104 179/179、ui_data 295/295、boot_stats 21/21、adc_cal 11
7/117、run_par 67/67;菜单 8 状态
池 5368 不变、内容页 53/53。**已知遗留**:内容页数 47 → 53 的全仓文本同步尚未做(另卡跟踪),本笔只保
证代码与黄金一致;面板注键真机那一腿缺 CLI,交台架道补。
2026-10-27 11:29:23 +08:00

373 lines
15 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.
/*
* 模块名称:运行参数段(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 <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(卡 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(&reg, 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(&reg) != 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;
}