Files
STM32F4-Base/App/app/ui_data_feed.c
T
wandering1 77779d49d6 feat(ui+记录): 整定写路径与四类页落地,五类记录写入方补齐
UI 面:ui_data 增加深度 4 参数写队列(请求 168 B、队列 .bss 672 B)与状态机
OK/ERR_RANGE/ERR_LOCKED/ERR_BUSY,ui_data_feed 出队后按注入 ops 走「闭锁 → 整批
预检 → 逐项 prot_set_set_sealed」,只提交改动位图;par 帧尾追加 cal_block
(口径 = UA/UN/IA 三路 ADC_CAL_F_VALID,与 protect_task 判据同构,不用 rt.eng
反推,V0.1 下 eng 恒 0)、UI_DATA_VER 升到 2。参数/定值/控制字/软压板四叶绑定
内容页(UI_PAGE_MAP_COUNT 12→16,复用四个查看模块,零新增 .c),编辑态按
is_editable 分派,LOCK(写不进去)与「未标定」(仍可改值)两源两通道两词。

记录面:syslog_request_clear/state/apply 三段式删除 API(UI 只发请求,落地在
defaultTask 的 drain 尾部且必须后置,卷锁一律 try,幂等只认 FR_NO_FILE);
事故/告警/启动/操作/遥控五类各有一个真实写入点,记录类型 = 日志 ID 本身
(一类一个 /log 严格 8.3 文件;64B 定长体没有类型字段位,塞字段会砸掉既有读者)。
为守 protectTask 872 B 静态最坏链另拆 syslog_post_quiet()(无 DBG 的投递核,
丢弃留痕挪到 drain 侧差值补发),syslog_post 对外语义逐字不变。

为什么两道合成一笔而不是各一笔(实测,不是省事):simulator/main.c 同时消费
两道新增 API(整定 pump 与 logtest 清除用例),按道切文件会让中间提交编不过;
RAM 受管快照与体积同码凭证描述的是合并后的 .map,拆开必落在「台账与 .map 不同
码」这条红线上。pre-commit 跑钩子前会 stash 未 staged 部分,这类耦合没法绕。

实测:@build 0 错误 0 警告;Code 228,260 / RO 24,960 / RW 1,440 / ZI 135,976 B;
RW_IRAM1 79,992 B = 61.0%、RW_CCM 87.9%(阈值 92%);九个任务静态最坏链全 PASS,
protectTask 872 B / 57% 改前后逐字节不变(记录投递没垫栈)。主机单测 ui_data 82→
164、sys_log_mcu 106→137、prot_elm 47→60、iec104 141→154、iec104_serv 28→34,
既有断言零删改;48 个钩子 --all-files 全绿;编号门禁可判定 86.34%(基线 86.0%)、
不可判定 1,153 处低于上限 1,166;doc-links 无法判定 0;UI 黄金 18 页扩到 27 页
连跑两次逐字节一致;字库 diff 空。

顺手修掉三处既有缺陷(都是本轮第一个消费者才暴露):sim_syslog.c 的 s_paths 行宽
16 而 "sim_selftest.log" 正好 16 字符没有 NUL 位(stat/remove 读越过数组尾,此前
靠后面 .rodata 碰巧为 0);ui_w_setlist 二态行 ENTER 不回绕(控制字与软压板出厂值
全在最后一项,按了像没反应而叠层里已是越界值);生成物 ui_menu_tab.h 与 gen_menu.py
模板漂移(上一笔改了模板没重跑生成器),现随生成器输出对齐。

未收口(不算完成):保存后重启值保持与主站 C_RD 读回两条只能上板,台架窗口执行;
LOCK 屏显与越界反显的页面侧在模拟器造不出来(模拟器不跑定值自检、无 NAND),
已由生成侧单测与代码注释点名差距;同类页数与钩子数陈述残留另立待办。
2026-10-26 15:51:12 +08:00

634 lines
30 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.
/*
* 模块名称:UI 数据组装器(ui_data_feed)
* 模块功能:App → UI 数据通道的写侧——按节拍从采集/标定/相量/测频/保护/协议/墙钟
* 各模块取数,组装成 docs/UI设计/UI_数据供给设计说明.md §4 契约的三帧
* (实时量 rt / 参数 par / 装置信息 info),经 Drivers/BSP/UI/ui_data.h
* 的发布 API 推给 UI 层;本模块是快照的**唯一填充者**,不含任何布局、
* 格式化与 LVGL 调用(显示规格全在页面行表,同文档 §8)
* 写方向(道F task-1):同一条 tick 顺带把页面投递的定值整定请求落定——
* ui_data_param_drain() 每轮取一条,执行动作经本文件的 s_param_ops
* (locked = prot_set_locked / check = g_prot_set_meta 上下限 /
* apply = prot_set_set_sealed)注入给 BSP;值域与落盘事实都留在 App
* 侧,BSP 只有队列与状态机(契约 §6.1"UI 不能直写"的落地形态)
* 量纲诚实性:eng 字段按**真值**填——docs/任务规划/数据可信.md §2.6 把承诺边界定在
* "引脚电压",故未做两点标定前 eng = UI_DATA_ENG_PIN(值 = ADC 引脚电压 V,
* 不是端子二次值);电气 8 路 adc_cal_flags() 的 VALID 全置位才升
* UI_DATA_ENG_SEC。TEMP 按决策不参与线性标定 → 恒带 UI_DATA_Q_RAW,
* 页面据此不挂单位(三态口径同文档 §4.2)
* 适用平台:STM32F407ZGTx(PC 模拟器不链接本文件,见 ui_data_feed.h 端接法)
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 王建锋 创建(道E task-14)
* v1.1 2026-10-26 DTU 项目组 道F task-1:① tick 前置 ui_data_param_drain()
* (定值写路径出队 → 闭锁/越界预检 → set_sealed,
* 见 s_param_ops);② 参数帧尾填 cal_block(未标定
* 闭锁徽标,三路口径同 protect_task.c,不用 eng)
*/
/* 头文件包含区 */
#include <string.h>
#include "cmsis_os.h" /* osKernelGetTickCount:与 adc_task 同一 ms 时基 */
#include "ui_data_feed.h"
/* 日志锚点:本文件只在"点表点数与快照点数不等"这一条静默漂移上打一条一次性日志
* (每条约 90 B Flash,见 AGENTS「日志不是免费的」——常规发布路径刻意一条都不加)。
* ⚠ DBG_TAG 必须在 include 之前定义:dbg_log.h 的 DBG_ERROR 是宏,漏 include 时
* ARMCC 只把 DBG_ERROR 当隐式声明函数放过编译期,错误落在链接期(L6218E
* Undefined symbol),比漏写头文件本身更难归因 */
#define DBG_TAG "[UIDATA]"
#include "dbg_log.h"
/* 下层(Drivers/BSP):快照存侧 + 两个端口/用户名常量来源(App → BSP 是合法方向) */
#include "ui_data.h"
#include "ui_net_sink.h" /* UI_NET_SINK_PORT:远程显示(RemoDispBus)监听端口 */
#include "ftp_cfg.h" /* FTP_CFG_USER_MAX / ftp_cfg_get() */
/* 同层(App)数据源 */
#include "adc_input.h" /* ADC_SEM_COUNT / adc_input_raw(TEMP 码值) */
#include "adc_cal.h" /* adc_cal_flags:VALID/CLIPPED 三态可用性 */
#include "phasor.h" /* 基波/谐波有效值(fAmSQ) */
#include "freq.h" /* 测频读数与新鲜度 */
#include "prot_elm.h" /* 元件动作态(遥信位图来源) */
#include "prot_set.h" /* 40 项定值 Q10 + 校验和 + 闭锁 + 越界位图 */
#include "sys_clock.h" /* 墙钟字符串 */
#include "iec104_serv.h" /* IEC104_SERV_PORT:实际监听端口(非协议层同名常量) */
#include "iec104_pts.h" /* task-15 批4:遥信点数与单/双点性质(点表事实的唯一来源) */
/* 私有宏定义区 */
/* 实时帧与信息帧发布节拍(ms);参数帧只在定值校验和变化时发布
* (节拍依据 docs/UI设计/UI_数据供给设计说明.md §5.2) */
#define FEED_RT_PERIOD_MS 200u
/* 遥信位号:**与 App/protocol/iec104/iec104_pts.c 板端源的位图同源**(该文件把
* 位号写死在函数体内、未导出,UI 侧不得 include 协议头,故在此镜像一份)。
* 变更纪律:104 点表位号改动时必须同步这里 + 页面名表 + 主机一致性测试
* (test/host/ui_data 的位图断言)。V0.1 有硬件来源的只有以下 6 位,
* 其余 27 位恒无效(开入未实现);过压/欠压/过频/低频四个元件在
* V0.1 点表里没有对应遥信点,故本函数不产出。 */
#define FEED_YX_BIT_FAULT_ALL 14u /* YX15 故障总(任一电压/电流段或零序动作) */
#define FEED_YX_BIT_OC1 15u /* YX16 过流 I 段 */
#define FEED_YX_BIT_OC2 16u /* YX17 过流 II 段 */
#define FEED_YX_BIT_OC3 17u /* YX18 过流 III 段 */
#define FEED_YX_BIT_OL 22u /* YX23 过载(过负荷) */
#define FEED_YX_BIT_LOCK 30u /* YX31 保护闭锁(定值校验和自检失败) */
/* YX1~4(bit0..3)是开关位置类双点点,V0.1 无开入硬件 → 恒 0 且不在有效位图内,
* 因此本文件不为它们定义常量(定义了也不参与任何判定 = 看着像接了线其实没有) */
/* 有效位图:YX15~18/23/31(与 104 点表 YX_VALID_MASK 同集合) */
#define FEED_YX_VALID_MASK ((((uint64_t)1u << FEED_YX_BIT_FAULT_ALL) | \
((uint64_t)1u << FEED_YX_BIT_OC1) | \
((uint64_t)1u << FEED_YX_BIT_OC2) | \
((uint64_t)1u << FEED_YX_BIT_OC3) | \
((uint64_t)1u << FEED_YX_BIT_OL) | \
((uint64_t)1u << FEED_YX_BIT_LOCK)))
/*
* 装置身份(版本类):仓库内**没有**产品级版本登记表,值沿用页面既有显示值
* (docs/UI设计/内容页架构.md 的装置信息行表现值),有正式口径后只改本段一处。
* ⚠ 软件校验不抄参考项目遗留的 "4454":构建期没有 CRC 生成步骤,如实显示 "--"
* (处置口径 docs/UI设计/页面扩充详细方案.md §3.7 第 5 行)
*/
#define FEED_MODEL "F30"
#define FEED_SW_VER "SV0.010"
#define FEED_HW_VER "HW0.010"
#define FEED_SW_SUM "--"
/* 编译期一致性守卫(ARMCC C99 无 _Static_assert,用项目惯用的数组宽度法,
* 先例 App/cfg/cfg_file.c 与 Drivers/BSP/UI/ui_menu_tab.h):
* 快照下标空间与各 App 枚举一旦漂移,编译即失败,不留到运行期
* (契约 docs/UI设计/UI_数据供给设计说明.md §9.4 第 1/2 条) */
typedef char feed_chk_yc_n_t[(UI_DATA_YC_N == (uint32_t)ADC_SEM_COUNT) ? 1 : -1];
typedef char feed_chk_set_n_t[(UI_DATA_SET_N == (uint32_t)PS_ID_COUNT) ? 1 : -1];
/* 写路径注入表的 id 形参是 uint8_t(BSP 侧不透明编号宽度,位图只有 64 位):
* 定值项数一旦破 64/255,request 的 mask 与 check/apply 的编号都会静默截断,
* 故在写侧也钉一道(BSP 侧同一条守卫在 Drivers/BSP/UI/ui_data.c) */
typedef char feed_chk_set_u8_t[((uint32_t)PS_ID_COUNT <= 64u) ? 1 : -1];
typedef char feed_chk_fuser_t[(UI_DATA_FUSER_LEN >= FTP_CFG_USER_MAX) ? 1 : -1];
/* 静态变量定义区 */
/* 组装缓冲:三帧串行填充,共用一块存储,省两份 RAM。
* 实际最大者是**实时帧**(208 B > par 192 B > info),故给 par 尾追加字段
* 只要不超过 rt 就一字节 RAM 都不多花(这条由主机单测 TC-UIDATA-021g/h 钉住,
* 尺寸一变这里就是唯一会撒谎的地方) */
static union {
ui_data_rt_t rt;
ui_data_par_t par;
ui_data_info_t info;
} s_scr;
/* 语义通道 → 基波相量实例下标缓存(-1 = 该通道无基波实例 → 该路发 IV);
* 首帧惰性建表(相量实例表是 ROM 表,运行期不变,建一次即可) */
static int8_t s_yc_phasor[ADC_SEM_COUNT];
static uint8_t s_map_done;
/* 发布节拍与参数变化检测状态
* par 帧有**三个各自独立**的变化源,任一变了就要重发一帧(详见 tick 里的门控注释):
* s_par_sum 定值改动(校验和)
* s_par_locked 定值闭锁(L5-1 自检失配,校验和不动)
* s_par_cal_block 未标定闭锁(卡 12 标定完成,校验和也不动)
* 后两个各占一字节——省下来就是"屏上状态与真值脱拍"这笔债 */
static uint32_t s_last_rt_ms;
static uint32_t s_par_sum;
static uint8_t s_par_sum_valid;
static uint8_t s_par_locked;
static uint8_t s_par_cal_block;
static uint8_t s_first_done;
/* 函数定义区 */
/*
* 函数功能:定长缓冲内拷贝字符串(跨层快照只允许定长数组,禁指针)
* 入口参数:p_dst - 目标缓冲(结构体内数组首地址) char *
* dst_len - 目标容量(含 NUL) size_t
* a_src - 源串(Flash 字面量或上层缓冲) const char *
* 返回值:无
* 限定条件:三者非 NULL 且 dst_len > 0;截断时保证 NUL 结尾
* 函数说明:不用 strcpy/sprintf(门禁 CODE-RULE-06 禁用不安全函数),
* 超长**静默截断**——快照字段本就是定长显示串,截断只会少显示尾部,
* 不会越界;本函数每 200 ms 调用个位数次,不为此加日志
*/
static void feed_copy_str(char *p_dst, size_t dst_len, const char *a_src) {
size_t n;
if ((p_dst == NULL) || (a_src == NULL) || (dst_len == 0u)) {
return;
}
n = strlen(a_src);
if (n > (dst_len - 1u)) {
n = dst_len - 1u;
}
(void)memcpy(p_dst, a_src, n);
p_dst[n] = '\0';
}
/*
* 函数功能:建立"语义通道 → 基波相量实例"映射(首帧一次)
* 入口参数:无
* 返回值:无
* 限定条件:uiTask 上下文,ui_data_feed_tick 首帧前调用
* 函数说明:只认 harm == 1 的实例;一个语义通道有多个基波实例时取**首个**
* (现状 k_inst[] 每通道最多一个基波,取首个即唯一解)。
* UB/UC/IB/IC/IN 无实例(V0.1 硬件削顶:双转换器无共同采样触发)→ 留 -1,该路发 IV
*/
static void feed_map_phasor(void) {
uint8_t n;
uint8_t i;
for (i = 0u; i < (uint8_t)ADC_SEM_COUNT; i++) {
s_yc_phasor[i] = -1;
}
n = phasor_inst_count();
for (i = 0u; i < n; i++) {
const phasor_inst_t *p = phasor_inst(i);
if ((p == NULL) || (p->harm != 1u) || ((uint8_t)p->sem >= (uint8_t)ADC_SEM_COUNT)) {
continue;
}
if (s_yc_phasor[(uint8_t)p->sem] < 0) {
s_yc_phasor[(uint8_t)p->sem] = (int8_t)i;
}
}
s_map_done = 1u;
}
/*
* 函数功能:算整帧量纲口径 eng
* 入口参数:无
* 返回值:UI_DATA_ENG_PIN 或 UI_DATA_ENG_SEC uint8_t
* 限定条件:无(纯读)
* 函数说明:**全或无**——电气 8 路(TEMP 除外,它不参与线性标定)的
* ADC_CAL_F_VALID 全置位才升二次值。取全或无是因为整帧共用一个 eng:
* 只要还有一路是引脚电压,页面挂"二次值"就会把那一路的读数误标
* (宁可整体显示"未标定",也不给部分真值贴真值标签)
*/
static uint8_t feed_eng(void) {
uint8_t sem;
for (sem = 0u; sem < (uint8_t)ADC_SEM_TEMP; sem++) {
if ((adc_cal_flags((adc_semantic_t)sem) & ADC_CAL_F_VALID) == 0u) {
return UI_DATA_ENG_PIN;
}
}
return UI_DATA_ENG_SEC;
}
/*
* 函数功能:组装遥信双点位图(bit i = 第 i 点是双点型 M_DP)
* 入口参数:无
* 返回值:位图 uint64_t
* 限定条件:uiTask 上下文;点数上限 UI_DATA_YX_N(<64,故位图放得下)
* 函数说明:逐点问点表 `iec104_pts_yx_is_dp()`,**不在此复制"前 4 点是双点"这个事实**
* ——那句话写在这里就是第二真相,点表一改就悄悄错(下发位图的理由见
* ui_data.h 的 yx_dp 字段注释)
*/
static uint64_t feed_yx_dp(void) {
uint64_t m = 0u;
uint8_t i;
for (i = 0u; (i < (uint8_t)UI_DATA_YX_N) && (i < 64u); i++) {
if (iec104_pts_yx_is_dp(i) != 0u) {
m |= ((uint64_t)1u << i);
}
}
return m;
}
/*
* 函数功能:组装遥信位图(bit i = 第 i 点"合/动作")
* 入口参数:无
* 返回值:位图 uint64_t
* 限定条件:uiTask 上下文(读 protectTask 写的元件状态快照,读侧拷贝语义,
* 与 App/protocol/iec104/iec104_pts.c 板端源同形)
* 函数说明:位号镜像本文件 FEED_YX_BIT_* 一组宏的注释(含同步纪律);
* 双点遥信 YX1~4 无硬件 → 恒 0 且不在有效位图内
*/
static uint64_t feed_yx_bits(void) {
prot_elm_state_t st;
uint64_t bits = 0u;
uint8_t fault_all;
fault_all = 0u;
prot_elm_state(PE_OC1, &st);
if (st.asserted != 0u) {
bits |= ((uint64_t)1u << FEED_YX_BIT_OC1);
fault_all = 1u;
}
prot_elm_state(PE_OC2, &st);
if (st.asserted != 0u) {
bits |= ((uint64_t)1u << FEED_YX_BIT_OC2);
fault_all = 1u;
}
prot_elm_state(PE_OC3, &st);
if (st.asserted != 0u) {
bits |= ((uint64_t)1u << FEED_YX_BIT_OC3);
fault_all = 1u;
}
prot_elm_state(PE_3U0, &st);
if (st.asserted != 0u) {
fault_all = 1u;
}
if (fault_all != 0u) {
bits |= ((uint64_t)1u << FEED_YX_BIT_FAULT_ALL);
}
prot_elm_state(PE_OL, &st);
if (st.asserted != 0u) {
bits |= ((uint64_t)1u << FEED_YX_BIT_OL);
}
if (prot_set_locked() != 0u) {
bits |= ((uint64_t)1u << FEED_YX_BIT_LOCK);
}
return bits;
}
/*
* 函数功能:组装实时量快照(遥测 9 路 + 相量实例槽 + 遥信 + 频率 + 墙钟)
* 入口参数:p_snap - 目标帧(调用方已清零) ui_data_rt_t *
* tick_ms - 发布时刻(tick 域) uint32_t
* 返回值:无
* 限定条件:uiTask 上下文;映射表已建(feed_map_phasor)
* 函数说明:取数口径与 104 上送同源不同路——本函数读**相量使用区**(1 ms 刷新、
* 双区原子交换),不做死区与平均;遥测链快照(docs/任务规划/数据可信.md
* §4.5 的 meas 快照)落地后,本函数改成读该快照即可,页面零改动
* (这正是本层存在的意义:换数据源只改这里)
*/
static void feed_fill_rt(ui_data_rt_t *p_snap, uint32_t tick_ms) {
uint32_t now_ep;
uint8_t sem;
char ts[UI_DATA_CLOCK_LEN];
p_snap->tick_ms = tick_ms;
p_snap->valid = 1u;
p_snap->eng = feed_eng();
/* 遥测:语义通道序的基波有效值(采样环已按 adc_cal 折算 → 当前为引脚电压域) */
for (sem = 0u; sem < (uint8_t)ADC_SEM_COUNT; sem++) {
int8_t pi = s_yc_phasor[sem];
p_snap->yc[sem].val = 0.0f;
p_snap->yc[sem].qds = 0u;
if (sem == (uint8_t)ADC_SEM_TEMP) {
/* 温度:不参与线性标定,透传转换器码值并打 RAW 位(页面不得挂单位) */
int32_t code = 0;
p_snap->yc[sem].qds = UI_DATA_Q_RAW;
if (adc_input_raw((adc_semantic_t)sem, &code) == 0) {
p_snap->yc[sem].val = (float)code;
} else {
p_snap->yc[sem].qds |= UI_DATA_Q_IV;
}
continue;
}
if (pi < 0) {
/* 无基波实例 = V0.1 该通道无可用采样源(削顶) → 无效 */
p_snap->yc[sem].qds = UI_DATA_Q_IV;
} else {
phasor_t ph;
int rc = phasor_get((uint8_t)pi, &ph);
if (rc == 0) {
p_snap->yc[sem].val = ph.fAmSQ; /* 有效值口径(系数已折 sqrt2/N) */
} else if (rc == -2) {
p_snap->yc[sem].qds = UI_DATA_Q_NT; /* 未攒满一周波 */
} else {
p_snap->yc[sem].qds = UI_DATA_Q_IV;
}
}
if ((adc_cal_flags((adc_semantic_t)sem) & ADC_CAL_F_CLIPPED) != 0u) {
p_snap->yc[sem].qds |= UI_DATA_Q_OV; /* 硬件削顶:交流量不可信 */
}
}
/* 相量实例槽:含谐波(Ia 二次/五次),交流量页的 Ia2/Ia5 行走这条带 */
{
uint8_t n = phasor_inst_count();
if (n > UI_DATA_PH_MAX) {
n = UI_DATA_PH_MAX; /* 超出槽上限的实例本帧不发布(现 5 实例,余 3 槽) */
}
p_snap->ph_n = n;
for (sem = 0u; sem < UI_DATA_PH_MAX; sem++) {
p_snap->ph[sem].val = 0.0f;
p_snap->ph[sem].qds = UI_DATA_Q_IV;
}
for (sem = 0u; sem < n; sem++) {
phasor_t ph;
if (phasor_get(sem, &ph) == 0) {
p_snap->ph[sem].val = ph.fAmSQ;
p_snap->ph[sem].qds = 0u;
} else {
p_snap->ph[sem].qds = UI_DATA_Q_NT;
}
}
}
p_snap->yx_bits = feed_yx_bits();
p_snap->yx_valid = FEED_YX_VALID_MASK;
p_snap->yx_dp = feed_yx_dp();
/* 点数漂移检查(只在真漂走时打一条日志):点表 YX_N 与本层 UI_DATA_YX_N 一旦不等,
* 页面按 33 行画、协议按另一个数上送,两边都"看起来正常"——这是静默错,值得花一条
* DBG(约 90 B Flash)换一次现场可见的自证;只打一次不刷屏(同丢弃计数口径) */
{
static uint8_t s_yx_n_bad;
uint8_t n = iec104_pts_yx_count();
if ((n != (uint8_t)UI_DATA_YX_N) && (s_yx_n_bad == 0u)) {
s_yx_n_bad = 1u;
DBG_ERROR("yx point count drift: pts=%u ui_name_table=%u",
(unsigned)n, (unsigned)UI_DATA_YX_N);
}
}
/* 测频:读数新鲜度单独走 freq_valid()(幅值门 + 收敛),两者任一不成立
* 页面即显示 "--"——eng=0 期间幅值门恒失效,读数本就不可消费,如实反映 */
{
float hz = 0.0f;
p_snap->freq_valid = 0u;
p_snap->freq_hz = 0.0f;
if (freq_get(&hz) == 0) {
p_snap->freq_hz = hz;
if (freq_valid() != 0u) {
p_snap->freq_valid = 1u;
}
}
}
/* 墙钟:UINT32_MAX 是 sys_clock_get() 的失败哨兵 → clock_ok=0、串清空 */
p_snap->clock_ok = 0u;
p_snap->clock[0] = '\0';
now_ep = sys_clock_get();
if ((now_ep != UINT32_MAX) && (sys_clock_get_str(ts, sizeof(ts)) != NULL)) {
feed_copy_str(p_snap->clock, sizeof(p_snap->clock), ts);
p_snap->clock_ok = 1u;
}
}
/* ── 写路径(契约 §6.2):把 prot_set 的三个动作包成注入给 BSP 的函数表 ────── */
/*
* 函数功能:写路径判定"定值闭锁中吗"(BSP 经 ui_data_param_ops_t 来问)
* 入口参数:无
* 返 回 值:1 - 闭锁(定值校验和自检失败态,禁一切定值写入);0 - 可写 uint8_t
* 限定条件:uiTask 上下文(只读 prot_set 状态位)
* 函数说明:这里的"闭锁"是**定值闭锁**(= par.locked = YX31 遥信),**不是**未标定
* 闭锁(= par.cal_block):后者仍可改定值、只是判据停走,两者在屏上不得
* 互认(三词三源口径 docs/任务规划/数据可信.md §4.3 + 落地简报 §7-6)
*/
static uint8_t feed_param_locked(void) {
return prot_set_locked();
}
/*
* 函数功能:写路径整批预检——单项是否落在 meta 上下限内
* 入口参数:a_id - 定值项编号(位图位序;uint8_t 是 BSP 侧不透明编号的宽度)
* a_q10 - 待写 Q10 定点值
* 返 回 值:0 - 合法;-1 - 编号越界或值越界 int
* 限定条件:只读 g_prot_set_meta(ROM 表),任意上下文安全
* 函数说明:上下限**只此一处取**——BSP 行表(Drivers/BSP/UI/ui_w_setview.c)是它的
* 换算镜像,由 setview-check 钩子核一致;本函数再抄一份数字就是第二真相
* ("查看合法、整定越界"两套边界就是这么来的)
*/
static int feed_param_check(uint8_t a_id, int32_t a_q10) {
if (a_id >= (uint8_t)PS_ID_COUNT) {
return -1;
}
if ((a_q10 < g_prot_set_meta[a_id].min) || (a_q10 > g_prot_set_meta[a_id].max)) {
return -1;
}
return 0;
}
/*
* 函数功能:写路径落值——单项**原子**写入 + 重封校验和 + 置脏
* 入口参数:a_id - 定值项编号;a_q10 - Q10 定点值
* 返 回 值:0 - 已写入;-1 - 编号越界或 prot_set 拒写(原值不动) int
* 限定条件:uiTask 上下文;调用前 BSP 已整批预检通过(两条件与本函数一致)
* 函数说明:用 prot_set_set_sealed()(影子拷贝→改→重封→单字交换,**每项自带置脏**,
* 落盘由 defaultTask 50 ms 圈扫脏写 /prot_set.bin,本模块不碰 FatFs)。
* ⚠ 不得改成 prot_set_set() + 批末 prot_set_seal():两条路径之间有
* "已改未封存"窗口,prot_set.c 明令不得交错,撞上就被 L5-1 自检判成
* "定值被非法改写"→ 整台保护闭锁
*/
static int feed_param_apply(uint8_t a_id, int32_t a_q10) {
if (a_id >= (uint8_t)PS_ID_COUNT) {
return -1;
}
return prot_set_set_sealed((prot_set_id_t)a_id, a_q10);
}
/* 注入给 ui_data_param_drain() 的执行侧实现(BSP 不保存副本,每次调用现传) */
static const ui_data_param_ops_t s_param_ops = {
feed_param_locked,
feed_param_check,
feed_param_apply
};
/*
* 函数功能:未标定闭锁判定(整定页"未标定"徽标的数据源)
* 入口参数:无
* 返 回 值:1 - 未标定闭锁(UA/UN/IA 三路至少一路标定 VALID 未置位);0 - 三路全
* VALID(保护判据正在跑) uint8_t
* 限定条件:uiTask 上下文(adc_cal_flags 纯读,每 200 ms 一次,代价可忽略)
* 函数说明:判据与 App/task/protect_task.c 的 s_cal_locked() **逐字同构**——那是
* 停走判据的真源,本函数只是把它搬到显示通道上;两处的例外通道也必须
* 一起改(DTU_BENCH 台架旁路,仅本地台架自行定义、产品恒强制)。
* ⚠ 已知重复:本仓没有导出该判据的公开 API(protect_task.h 只有线程
* 入口),故这里镜像一份;将来 protect_task 导出访问器后应收敛成一处
* (Lead 补充 A 明确本任务按此实现,不改 protect_task.*)
* ⚠ 不得用 rt.eng 反推:eng 要求电气 8 路全 VALID,V0.1 双转换器下
* UB/UC/IB/IC/IN 永不置位 → eng 恒为引脚档,照它画徽标就永不解锁
*/
static uint8_t feed_cal_block(void) {
/* 形态与 protect_task.c 同:用 #ifndef 把产品默认路径留在主分支(而非 #ifdef
* 双分支)——cppcheck 对未定义宏取真分支,双分支会把它引到旁路上报假信号 */
#ifndef DTU_BENCH
if (((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) == 0u) ||
((adc_cal_flags(ADC_SEM_UN) & ADC_CAL_F_VALID) == 0u) ||
((adc_cal_flags(ADC_SEM_IA) & ADC_CAL_F_VALID) == 0u)) {
return 1u;
}
#endif
return 0u;
}
/*
* 函数功能:组装参数快照(40 项定值 Q10 + 校验和 + 封存态 + 越界位图 + 未标定闭锁)
* 入口参数:p_snap - 目标帧(调用方已清零) ui_data_par_t *
* sum - 本次发布的 prot_set_checksum() 值 uint32_t
* 返回值:无
* 限定条件:uiTask 上下文(读侧;写者 = iec104Task 的 set_sealed 与装载路径,
* 读到的永远是"值区与封存一致"的完整快照)
* 函数说明:只搬 Q10 原值不换算小数位——换算属表现层
* (docs/UI设计/UI_数据供给设计说明.md §8)
*/
static void feed_fill_par(ui_data_par_t *p_snap, uint32_t sum) {
uint8_t i;
p_snap->checksum = sum;
p_snap->valid = 1u;
p_snap->locked = prot_set_locked();
p_snap->bad_mask = prot_set_bad_mask();
/* 越界位图非 0 = 上一次整表装载/提交有越界项(prot_set_bad_mask 的既有口径),
* 页面据此反显;为 0 时无意义位图不参与显示 */
p_snap->bad_valid = (p_snap->bad_mask != 0u) ? 1u : 0u;
/* v2 尾追加:未标定闭锁徽标(三路口径,与判据停走同源;与上面的 locked 是
* 两件事——locked 禁编辑,cal_block 只说明"改了也不会动作") */
p_snap->cal_block = feed_cal_block();
for (i = 0u; i < (uint8_t)PS_ID_COUNT; i++) {
p_snap->q10[i] = prot_set_get((prot_set_id_t)i);
}
}
/*
* 函数功能:组装装置信息快照(身份 + 运行时长 + 通讯参数)
* 入口参数:p_snap - 目标帧(调用方已清零) ui_data_info_t *
* tick_ms - 当前 tick(换算 uptime_s 用) uint32_t
* 返回值:无
* 限定条件:uiTask 上下文
* 函数说明:页面不得自己 include 协议/配置头取这些值,一律经本帧下发
* (docs/UI设计/页面扩充详细方案.md §3.5"值必须在 App 侧取");
* 公共地址 CA 不在此下发——固件里 CA 是逐帧从 ASDU 头解析的,
* 没有集中常量可取,待 104 侧给出查询点后按扩展规则追加字段
*/
static void feed_fill_info(ui_data_info_t *p_snap, uint32_t tick_ms) {
const ftp_cfg_t *p_ftp = ftp_cfg_get();
p_snap->valid = 1u;
p_snap->uptime_s = tick_ms / 1000u;
p_snap->port_104 = (uint16_t)IEC104_SERV_PORT;
p_snap->port_disp = (uint16_t)UI_NET_SINK_PORT;
feed_copy_str(p_snap->model, sizeof(p_snap->model), FEED_MODEL);
feed_copy_str(p_snap->sw_ver, sizeof(p_snap->sw_ver), FEED_SW_VER);
feed_copy_str(p_snap->hw_ver, sizeof(p_snap->hw_ver), FEED_HW_VER);
feed_copy_str(p_snap->build_date, sizeof(p_snap->build_date), __DATE__);
feed_copy_str(p_snap->sw_sum, sizeof(p_snap->sw_sum), FEED_SW_SUM);
if (p_ftp != NULL) {
feed_copy_str(p_snap->ftp_user, sizeof(p_snap->ftp_user), p_ftp->user);
} else {
p_snap->ftp_user[0] = '\0';
}
}
/*
* 函数功能:装配节拍入口——先落定一条定值整定请求(写方向),再发布实时帧与
* 信息帧(满 200 ms 一次)、参数帧(仅校验和变化时);首轮立即发布
* (免启动初期出现一闪的 "--")
* 入口参数:无
* 返回值:无
* 限定条件:uiTask 上下文,主循环每轮一次(UI 循环周期 10 ms)
* 函数说明:每轮未到期时代价 = 一次 osKernelGetTickCount + 两次减法比较 +
* 一次空队列判断(drain 无请求时只是一次计数比较);
* 差值法吸收 tick 回绕(同 App/algo/adc_health.h 的 now_ms 口径)。
* 写路径失败态(ERR_RANGE/ERR_LOCKED/ERR_BUSY)走屏上通道,不打日志
* (每条 DBG 约 90 B Flash + 216 B 栈,AGENTS「日志不是免费的」)
*/
void ui_data_feed_tick(void) {
uint32_t now;
uint32_t sum;
uint8_t due_rt;
now = osKernelGetTickCount();
if (s_map_done == 0u) {
feed_map_phasor();
}
/* 写路径:先落定整定请求,**再**取校验和——本轮刚写进 prot_set 的定值就进
* 本轮"校验和变化才发"那道门,页面保存行的 OK 与新值同一帧回来,不必多等
* 一轮(10 ms)。顺序反了不出错,只是慢一轮。
* 每轮最多处理一条(ui_data_param_drain 的限定条件):整定是人工低频事件,
* 一轮一条已经把最坏耗时钉住,不必排空整个队列
* ⚠ 返回值必须显式丢弃:本函数不消费"处理了几条",但 bsp-err 的
* consume-result 只认接住或 (void) 两种写法(实测编译器与 cppcheck 都不报) */
(void)ui_data_param_drain(&s_param_ops);
sum = prot_set_checksum();
due_rt = 0u;
if ((s_first_done == 0u) || ((uint32_t)(now - s_last_rt_ms) >= FEED_RT_PERIOD_MS)) {
due_rt = 1u;
s_first_done = 1u;
s_last_rt_ms = now;
}
if (due_rt != 0u) {
(void)memset(&s_scr, 0, sizeof(s_scr));
feed_fill_rt(&s_scr.rt, now);
(void)ui_data_publish_rt(&s_scr.rt);
(void)memset(&s_scr, 0, sizeof(s_scr));
feed_fill_info(&s_scr.info, now);
(void)ui_data_publish_info(&s_scr.info);
}
/* 参数帧门控:定值变化是低频事件,不随 200 ms 白拷一帧(契约 §5.2)。
* 但"校验和没变"不等于"页面该看的没变"——本帧里还有两个与校验和无关的事实:
* · locked(定值闭锁):自检失配时置位,校验和一字不变,
* 而整定页保存行要显示 LOCK 并禁编辑;
* · cal_block(未标定闭锁):卡 12 两点标定写完 [adc_cal] 的 valid 就翻转,
* 校验和同样不动,而标题上的"未标定"徽标要当场消失。
* 只盯校验和,这两件事都到不了屏上(真机现象:标定做完了屏上徽标永远挂着,
* 直到某次定值改动才被顺带刷掉——那是把两件不相干的事当同步点)。
* 代价:每轮多两次字节比较(无变化时零拷贝),换来屏上状态与真值同拍 */
{
uint8_t locked_now = prot_set_locked();
uint8_t cal_now = feed_cal_block();
if ((s_par_sum_valid == 0u) || (sum != s_par_sum)
|| (locked_now != s_par_locked) || (cal_now != s_par_cal_block)) {
(void)memset(&s_scr, 0, sizeof(s_scr));
feed_fill_par(&s_scr.par, sum);
(void)ui_data_publish_par(&s_scr.par);
s_par_sum = sum;
s_par_locked = locked_now;
s_par_cal_block = cal_now;
s_par_sum_valid = 1u;
}
}
}