Files
STM32F4-Base/Drivers/BSP/UI/ui_data.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

423 lines
18 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)
* 模块功能:三通道快照(实时量 rt / 参数 par / 装置信息 info)的存储、发布盖章与
* 只读访问——本模块是纯内存叶子:不取数、不格式化、不判断业务含义、
* 不碰 LVGL、不 include 任何 App 头(职责边界与依赖方向 =
* Drivers/BSP/UI/ui_data.h 文件头与
* docs/UI设计/UI_数据供给设计说明.md §3.2)
* 并发模型:单写者同上下文——唯一发布者 App/app/ui_data_feed.c 与全部页面钩子都在
* uiTask 内串行执行,故无锁、无双缓冲、无 seqlock;将来真出现第二个
* 发布者,保护收在本模块内部(双槽 + 每槽 seq + 单字节活动下标,照
* App/algo/phasor.c 双区先例),页面代码不受影响
* (判据 docs/UI设计/UI_数据供给设计说明.md §5.1)
* 写路径(定值整定请求队列)同理:生产者是页面"保存"按键钩子、消费者是
* App 侧节拍里的 ui_data_param_drain(),两者同在 uiTask → 环形队列照旧
* 无锁;若将来出现第二个投递上下文,唯一要做的是给 request/drain 两侧
* 各加一把互斥(队列本身是单字 head/cnt 推进,页面侧不会看到半成品)
* 模块功能补充:除三通道快照外,本模块还持有定值写路径的请求队列与状态字
* (ui_data_param_request/state/drain)——本层只做"排队 + 状态机",
* 不判断值域、不碰 prot_set、不碰文件系统(值域与写入事实经
* ui_data_param_ops_t 由 App 侧注入,理由见 ui_data.h 写路径节)
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 王建锋 创建(道E task-14)
* v1.1 2026-10-26 DTU 项目组 道F task-1:新增定值写路径环形队列(深度 4、满返 -1)
* 与状态机(IDLE/WAIT/OK/ERR_RANGE/ERR_LOCKED/ERR_BUSY),
* 执行经注入函数表回调 App 侧;参数帧新增 cal_block
* 字段随 publish 原样入存(本层不解释含义)
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_data.h"
/* 静态变量定义区 */
/* 三通道存储(上电全零 = "尚无首帧",与 ui_data_init() 后状态一致) */
static ui_data_rt_t s_rt;
static ui_data_par_t s_par;
static ui_data_info_t s_info;
/*
* 发布计数:seq 由本层盖章,与入帧携带的 seq 无关——入帧的该字段被覆盖。
* 读侧"有无首帧"的判据是计数为 0,不是帧内 valid:valid 是"这一帧有没有有效
* 数据"(写侧可能发布一帧全无效的真值),两者语义不同,混用会让"首帧之前"
* 与"首帧但无数据"在页面上显示成同一种态。
*/
static uint32_t s_rt_cnt;
static uint32_t s_par_cnt;
static uint32_t s_info_cnt;
/* ── 写路径:定值整定请求队列(深度 UI_DATA_PARAM_QD 的静态环形数组)──────── */
/*
* mask 合法位集合 = 低 UI_DATA_SET_N 位(40 项 → 0x000000FFFFFFFFFF)。
* 用"全 1 右移"而不是"1 左移 40 位再减 1":前者在 UI_DATA_SET_N == 64 时仍是
* 良定义(移 0 位),后者会退化成立即未定义行为(移位数 == 位宽)。
* 项数一旦超过 64,下面的编译期守卫当场拦下——位图放不下就是表示法该换,
* 绝不能静默把高 24 项变成"投不进去也不报错"
*/
#define UI_DATA_PARAM_MASK_VALID ((uint64_t)(~(uint64_t)0u) >> (64u - UI_DATA_SET_N))
typedef char ui_data_chk_mask_t[((uint32_t)UI_DATA_SET_N <= 64u) ? 1 : -1];
/* 环形队列:head = 出队下标,cnt = 在队条数(0..UI_DATA_PARAM_QD)
* 为什么用 head+cnt 而不是 head+tail:cnt 直接就是"还有几条没落定",
* state() 判 WAIT 不必再做 (tail - head) 取模减法,少一处可算错的地方 */
static ui_data_param_req_t s_pq[UI_DATA_PARAM_QD];
static uint8_t s_pq_head;
static uint8_t s_pq_cnt;
/* 写路径状态字与越界位图(页面只读;int/uint64 单字读写在 M4 上不撕裂) */
static int s_pq_state = UI_DATA_PARAM_ST_IDLE;
static uint64_t s_pq_err_mask;
/* 函数定义区 */
/*
* 函数功能:快照层初始化——三通道清零(valid=0、seq=0)+ 写路径队列复位,
* 页面即按"无首帧/未投递"渲染
* 入口参数:无
* 返回值:无
* 限定条件:内核启动前可调(纯内存操作,零 OS 依赖);可重复调用(幂等)
* 函数说明:静态存储上电本已清零,本函数的意义是运行期复位(例如"恢复出厂后
* 重开 UI"这类后续路径),以及让宿主侧测试能随时造出"首帧之前"的状态。
* 写路径一并丢弃在队请求是**有意**的:旧请求带的是旧配置下的编辑叠层,
* 复位后还把它落进 prot_set,等于拿陈旧草稿覆盖新装载的定值
*/
void ui_data_init(void) {
(void)memset(&s_rt, 0, sizeof(s_rt));
(void)memset(&s_par, 0, sizeof(s_par));
(void)memset(&s_info, 0, sizeof(s_info));
s_rt_cnt = 0u;
s_par_cnt = 0u;
s_info_cnt = 0u;
(void)memset(s_pq, 0, sizeof(s_pq));
s_pq_head = 0u;
s_pq_cnt = 0u;
s_pq_state = UI_DATA_PARAM_ST_IDLE;
s_pq_err_mask = 0u;
}
/*
* 函数功能:发布一帧实时量快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧(本层整帧拷入自有存储,调用方可复用其栈/静态空间)
* const ui_data_rt_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:唯一发布者 = App/app/ui_data_feed.c,uiTask 上下文
* 函数说明:seq 由本层盖章(入帧 seq 被忽略);其余字段原样入存,本层不做任何
* 业务判断与格式化
*/
int ui_data_publish_rt(const ui_data_rt_t *a_snap) {
if (a_snap == NULL) {
return -1;
}
(void)memcpy(&s_rt, a_snap, sizeof(s_rt));
s_rt_cnt += 1u;
s_rt.seq = s_rt_cnt;
return 0;
}
/*
* 函数功能:发布一帧参数快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧 const ui_data_par_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:同上;发布节拍 = 定值校验和变化才发
* (docs/UI设计/UI_数据供给设计说明.md §5.2)
*/
int ui_data_publish_par(const ui_data_par_t *a_snap) {
if (a_snap == NULL) {
return -1;
}
(void)memcpy(&s_par, a_snap, sizeof(s_par));
s_par_cnt += 1u;
s_par.seq = s_par_cnt;
return 0;
}
/*
* 函数功能:发布一帧装置信息快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧 const ui_data_info_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:同上
*/
int ui_data_publish_info(const ui_data_info_t *a_snap) {
if (a_snap == NULL) {
return -1;
}
(void)memcpy(&s_info, a_snap, sizeof(s_info));
s_info_cnt += 1u;
s_info.seq = s_info_cnt;
return 0;
}
/*
* 函数功能:读最新一帧实时量快照(整帧拷给页面私有缓存)
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_rt_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动,页面显示 "--");
* -2 - 参数无效(NULL) int
* 限定条件:页面钩子上下文(uiTask);勿在逐行 refresh 里无条件整帧拷贝,
* 先比 ui_data_rt_seq()(用法见 Drivers/BSP/UI/ui_data.h 文件头)
*/
int ui_data_rt(ui_data_rt_t *a_out) {
if (a_out == NULL) {
return -2;
}
if (s_rt_cnt == 0u) {
return -1;
}
(void)memcpy(a_out, &s_rt, sizeof(s_rt));
return 0;
}
/*
* 函数功能:读最新一帧参数快照
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_par_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动);-2 - 参数无效 int
* 限定条件:同上;定值页同样用 ui_data_par_seq() 门控页面私有缓存
*/
int ui_data_par(ui_data_par_t *a_out) {
if (a_out == NULL) {
return -2;
}
if (s_par_cnt == 0u) {
return -1;
}
(void)memcpy(a_out, &s_par, sizeof(s_par));
return 0;
}
/*
* 函数功能:读最新一帧装置信息快照
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_info_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动);-2 - 参数无效 int
* 限定条件:同上
*/
int ui_data_info(ui_data_info_t *a_out) {
if (a_out == NULL) {
return -2;
}
if (s_info_cnt == 0u) {
return -1;
}
(void)memcpy(a_out, &s_info, sizeof(s_info));
return 0;
}
/*
* 函数功能:查实时帧发布序号(页面缓存失效判据)
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无(只读单字,任意上下文安全)
* 函数说明:轻量查询——只读一个 uint32,可逐行调用;序号只在发布时 +1,页面
* 据此把"整帧拷贝"从每帧每行降到只在换帧时一次
*/
uint32_t ui_data_rt_seq(void) {
return s_rt.seq;
}
/*
* 函数功能:查参数帧发布序号
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无
*/
uint32_t ui_data_par_seq(void) {
return s_par.seq;
}
/*
* 函数功能:查装置信息帧发布序号
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无
*/
uint32_t ui_data_info_seq(void) {
return s_info.seq;
}
/*
* 函数功能:投递一条定值写入请求(页面"保存"行的唯一出口——只投递,不执行)
* 入口参数:a_req - 请求(本层整条拷入环形队列,调用方可复用其栈/叠层空间)
* const ui_data_param_req_t *
* 返回值:0 - 已入队(状态转 WAIT);或 mask 为 0 的空请求(无改动可保存,
* 状态直接转 OK,不占槽位)
* -1 - 队列满(状态转 ERR_BUSY,在队请求一条不动);
* -2 - 参数无效:NULL 或 mask 含 UI_DATA_SET_N 以上的位
* (状态与队列都不动——把调用方的编程错误落成屏上"错误"会误导现场) int
* 限定条件:uiTask 上下文(页面按键钩子);与 drain 同上下文串行 → 无锁
* 函数说明:本层不校验值域(越界与否属执行侧事实,落在 drain 的整批预检),
* 不 include App 头、不碰 prot_set、不碰 FatFs(UI 直写定值会把校验和
* 打成中间态、直写盘会撞 defaultTask 单写者纪律——契约 §6.1)
* 空请求按成功处理是刻意的:保存行 ENTER 时"一个都没改"是正常操作,
* 报 ERR 会让用户以为设备有问题
*/
int ui_data_param_request(const ui_data_param_req_t *a_req) {
uint8_t slot;
if (a_req == NULL) {
return -2;
}
if ((a_req->mask & ~UI_DATA_PARAM_MASK_VALID) != 0u) {
return -2;
}
if (a_req->mask == 0u) {
s_pq_state = UI_DATA_PARAM_ST_OK;
s_pq_err_mask = 0u;
return 0;
}
if (s_pq_cnt >= (uint8_t)UI_DATA_PARAM_QD) {
s_pq_state = UI_DATA_PARAM_ST_ERR_BUSY;
return -1;
}
slot = (uint8_t)(((uint32_t)s_pq_head + (uint32_t)s_pq_cnt) % UI_DATA_PARAM_QD);
(void)memcpy(&s_pq[slot], a_req, sizeof(s_pq[slot]));
s_pq_cnt += 1u;
s_pq_state = UI_DATA_PARAM_ST_WAIT;
return 0;
}
/*
* 函数功能:取写路径状态(页面保存行显示)
* 入口参数:无
* 返回值:UI_DATA_PARAM_ST_IDLE / _WAIT / _OK / _ERR_RANGE / _ERR_LOCKED /
* _ERR_BUSY 之一 int
* 限定条件:无(只读单字,任意上下文安全)
* 函数说明:本函数就是 s_pq_state 的直读,不做"pending 优先"之类的二次加工——
* 三条写路径(投递成功→WAIT、投递被拒→ERR_BUSY、drain 落定→终态或
* 残留则 WAIT)已把顺序语义收在写侧,读侧再判一次等于两处解释同一件事
*/
int ui_data_param_state(void) {
return s_pq_state;
}
/*
* 函数功能:取最近一条请求的越界项位图(页面高亮越界行)
* 入口参数:无
* 返回值:bit i = 1 → 第 i 项越界;最近终态不是 ERR_RANGE 时恒 0 uint64_t
* 限定条件:无(只读单字)
* 函数说明:与 ui_data_par_t.bad_mask 两条来源不同(本函数 = 本次提交被预检挡下
* 的项;bad_mask = prot_set 整表装载/解析留下的越界位图),页面在
* ERR_RANGE 那一帧用本函数,混用会把上一次装载的越界显示成本次的
*/
uint64_t ui_data_param_err_mask(void) {
return s_pq_err_mask;
}
/*
* 函数功能:整批预检一条请求(位图内置位的项逐个问执行侧 check())
* 入口参数:a_ops - 执行侧函数表(调用方已核非 NULL)
* a_req - 待检请求
* 返回值:越界项位图(bit i = 第 i 项越界;0 = 本批全部在界内) uint64_t
* 限定条件:只调 check(),不写任何东西("预检"与"落值"分成两个函数,
* 正是为了让 all-or-nothing 有地方落脚:整批扫完才决定写不写)
* 函数说明:一次扫全 40 位而不是"遇到第一个越界就返回"——err_mask 要给页面
* **全部**越界行反显,只报第一行等于让用户改完一行再撞下一行
*/
static uint64_t ui_data_param_scan(const ui_data_param_ops_t *a_ops,
const ui_data_param_req_t *a_req) {
uint64_t bad = 0u;
uint64_t bit = 1u;
uint8_t i;
for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) {
if ((a_req->mask & bit) != 0u) {
if (a_ops->check(i, a_req->q10[i]) != 0) {
bad |= bit;
}
}
bit <<= 1u;
}
return bad;
}
/*
* 函数功能:逐项落值一条请求(调用前 ui_data_param_scan() 已确认整批在界内)
* 入口参数:a_ops - 执行侧函数表
* a_req - 待写请求
* 返回值:0 - 位图内全部写入;-1 - 某项被执行侧拒绝(*p_bad 输出该项) int
* 限定条件:apply() 的副作用由执行侧定义(App 实现 = prot_set_set_sealed,
* 每项自带置脏);本函数不回滚已写项——prot_set 没有回滚接口
* 函数说明:走到这里还失败 = 执行侧与预检不一致(两个真源漂移),理论不可达;
* 如实报失败项比谎称"整批未写"更诚实,故**可能已有前几项生效**
*/
static int ui_data_param_write(const ui_data_param_ops_t *a_ops,
const ui_data_param_req_t *a_req,
uint64_t *p_bad) {
uint64_t bit = 1u;
uint8_t i;
for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) {
if ((a_req->mask & bit) != 0u) {
if (a_ops->apply(i, a_req->q10[i]) != 0) {
*p_bad = bit;
return -1;
}
}
bit <<= 1u;
}
*p_bad = 0u;
return 0;
}
/*
* 函数功能:处理一条定值写入请求(App 装配层节拍入口:出队 → 闭锁 → 预检 → 落定)
* 入口参数:a_ops - 执行侧函数表(locked/check/apply 三者都不得为 NULL)
* const ui_data_param_ops_t *
* 返回值:1 - 处理并提交了一条(状态已转终态,或队列仍有残留则转 WAIT);
* 0 - 队列空(状态与队列都不动);
* -1 - 参数无效(a_ops 或其成员为 NULL,状态与队列都不动) int
* 限定条件:uiTask 上下文(App/app/ui_data_feed.c 的 tick 每轮一次);每轮最多
* 处理一条——整定是低频人工事件,一轮一条已够,且把最坏耗时钉在
* 2 × 40 次回调(预检一遍 + 写入一遍),不随队列堆积抖动
* 函数说明:四步顺序即判据(契约 §6.2),判定本体拆成 scan/write 两个静态函数
* (见其函数说明)——
* ① 取队首;② locked() → ERR_LOCKED 并**丢弃本条**(闭锁是业务态,不做
* "排队等解锁",否则屏上 WAIT 无限期挂着,用户以为设备卡死);
* ③ scan() 任一项越界 → ERR_RANGE 且**一字不写**(all-or-nothing,与
* prot_set_load_raw 同纪律:半张表落进值区就是"看着保存成功、其实混着
* 新旧两套定值");④ write() 全成 → OK。
* ⚠ 顺序不可调:先判闭锁再判越界——闭锁态下 check() 的语义仍成立,但
* 页面要的是"定值闭锁"这个更高优先级的原因(契约 §6.3 表序)
*/
int ui_data_param_drain(const ui_data_param_ops_t *a_ops) {
const ui_data_param_req_t *p_req;
uint64_t bad;
int settled;
if ((a_ops == NULL) || (a_ops->locked == NULL) ||
(a_ops->check == NULL) || (a_ops->apply == NULL)) {
return -1;
}
if (s_pq_cnt == 0u) {
return 0;
}
p_req = &s_pq[s_pq_head];
bad = 0u;
if (a_ops->locked() != 0u) {
settled = UI_DATA_PARAM_ST_ERR_LOCKED;
} else {
settled = UI_DATA_PARAM_ST_OK;
bad = ui_data_param_scan(a_ops, p_req);
if (bad != 0u) {
settled = UI_DATA_PARAM_ST_ERR_RANGE; /* 预检挡下:一字未写 */
} else {
if (ui_data_param_write(a_ops, p_req, &bad) != 0) {
settled = UI_DATA_PARAM_ST_ERR_RANGE; /* 执行侧拒写:见函数说明 */
}
/* 两关都过 → settled 保持 OK */
}
}
s_pq_err_mask = bad;
s_pq_head = (uint8_t)(((uint32_t)s_pq_head + 1u) % UI_DATA_PARAM_QD);
s_pq_cnt -= 1u;
s_pq_state = (s_pq_cnt != 0u) ? UI_DATA_PARAM_ST_WAIT : settled;
return 1;
}