/* * 模块名称: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 #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; }