Files
STM32F4-Base/App/task/ui_task.c
T
wandering1 dab0d8ba17 feat(屏板): 帧 B 组帧 + 中断起发 TX DMA + 按键队列 + 离线判定(批 2)
批 2 五项全部落地(docs/任务规划/屏板链路.md §3):
- 2-1 sb_link_frame_update():uiTask 渲染后按需组帧,不留 3200 B 影子
  副本,直接拿活显存与帧内点阵段 memcmp;组完自校验,过了才置 ready。
  静态 4208 B 帧缓冲实测落 RW_IRAM1(0x20006d98),不在 CCM。
- 2-2 空闲中断校完 6 B 召唤帧直接点 TX DMA(sb_tx_kick),不排队不攒批;
  busy 期间再来的召唤丢帧并计数,绝不叠发(屏板收帧一次 IDLE 一包)。
- 2-3 按键走环形队列(中断入、任务出),uiTask 按 §4.3 表映射 UI_KEY_*
  后调 ui_port_queue_key;9/10(RESET/FACTORY)按决策四先丢弃并留痕。
- 2-4 HAL_UART_TxCpltCallback 在 rs485.c 加第三行转发(三个全局回调
  落点收一处,防日后重复定义);busy 冻结帧缓冲,错误中断按 gState
  收拾被打断的 TX,防残留 busy 把链路永久锁死。
- 2-5 连续 >=10 s 无有效召唤帧才判离线(不按 500 ms——屏板约 5000 s
  自复位有 1 s 空窗),恢复即清标志;CRC 错帧静默丢弃并计数。

实测:台架 40 s(PC 顶替屏板,--key-fixed 2 持续改画面)rx=143 ok=143
keys=143 crc=0 / tx=143 done=143 drop=0 abor=0 fb=144 bad=0;脚本一停
10 s 准点出 OFFLINE。组帧耗时 509 µs 典型 / 709 µs 峰值——原 0.3 ms
判据系估算,已按实测在规划文档改口为 1 ms 并给出压降路径(未做)。

附带:rs485_on_rx 补 CODE-RULE-09 文档块(本轮改 rs485.c 触发整文件
重扫被拦);CRC 循环计数改 32 位(消掉 ARMCC 每轮 UXTH);bench 脚本
加 --reply/--key-fixed/--dump,并写明 CH340 收不了 700 kbaud 连续流、
--reply 收不全 4208 B 不是固件问题。

主机单测 146/146 PASSED(+65);Keil 0 错误 0 警告;ci_local 全绿
(SKIP=nested-comment,worktree 限制,主树脚本另查 0 新发)。
Code=238,936 B / RW_IRAM1 65.5 % / RW_CCM 87.9 %。
2026-10-29 20:59:36 +08:00

587 lines
32 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 Task
* 模块功能:UI 主任务,等待网络就绪后初始化显示/菜单/推流并周期刷新
* 适用平台:STM32F407 系列(LVGL + CH395F 以太网)
* 作者:王建锋
* 创建日期:2026-08-30
* 修改记录:
* v1.1 2026-09-14 王建锋 新增待机显示页初始化(ui_standby_init,空闲 60s 切入)
* v1.2 2026-10-10 王建锋 任务耗时测量埋点:主循环加常开探针
* (循环周期 / 单轮工作耗时 / ui_net_sink_poll 往返),
* 用于把"心跳缺席"拆成 饿死 / 阻塞在往返 / 自身拖长
* 三种互斥机理(判读见 docs/子系统设计/看门狗设计说明.md §11.4)
* v1.3 2026-10-10 王建锋 解耦心跳(docs/子系统设计/看门狗设计说明.md §11.3):把 task_wd_beat
* (TASK_WD_UI) 提到 ui_net_sink_poll() **之前**。原写法心跳
* 在推流往返之后 → 心跳周期 = 渲染 + 一次 netTask 往返 + 10ms,
* 实测 uiw≈uis≈50ms(心跳被 netTask 往返主导),心跳实际测的是
* "netTask 响应性"而非"UI 活着"。提到往返之前后,心跳测的是
* "UI 自己被调度到了"。注:若往返**永久**阻塞,本改仍不能救
* (心跳只发一次)→ 真要彻底去掉依赖须走事件驱动 accept
* (NET_EVENT_CONNECTED 回调),待复现快照确认阻塞机理后再定。
* v1.4 2026-10-25 DTU 项目组 F3(大窗判读):向 ui_net_sink 注册心跳回调
* (ui_task_sink_beat)——推流段内**每个可等待 net 往返**
* (accept/recv/send/close/重建)返回点即喂本任务心跳,
* 风暴期「单轮多个慢往返之和」不再串成 800ms 缺席链
* (2026-10-25 实测 bit4 8 连击、uisn=154ms 仍在段内;
* 按位连击判据 = docs/子系统设计/看门狗设计说明.md §5.3)。
* 单个往返自身仍无上界——该上界与彻底解耦(事件驱动
* accept)归登记簿「net 层跨上下文收口」(批 3),本票只做心跳续命。
* v1.5 2026-10-26 DTU 项目组 道F task-3 装置维护三页的执行侧:新增
* ui_data_maint_ops_t 六件适配(时钟=写+回读 RTC、
* 复归=prot_elm_request_reset、清除=syslog_request_clear
* /clear_state 直通)并在 ui_menu_init 之前注入;记录
* reader 的 2 条 if 链改成 7 条映射表 s_log_map,并加
* 编译期守卫把 UI_W_REC_LOG_* == syslog_id_t 枚举序钉死
*/
#include "ui_task.h"
#include <string.h>
#include "cmsis_os.h"
#include "sys_events.h"
#include "ui_port.h"
#include "ui_menu.h"
#include "ui_net_sink.h"
#include "ui_standby.h"
#include "ui_w_records.h" /* task-6 记录 reader 注入点(UI 页面不再 include sys_log.h) */
#include "ui_w_soe.h" /* task-15 SOE 记录 reader 注入点(同上:UI 不 include soe_store.h) */
#include <stdio.h> /* task-15 snprintf:CP56 → UI 视图里的 char[20] 时标 */
#include "ui_data.h" /* task-14 快照存侧(BSP):本任务只负责先复位再喂帧 */
#include "ui_data_feed.h" /* task-14 数据装配节拍入口(App/app,UI 侧唯一发布者) */
#include "sys_log.h"
#include "soe_store.h" /* task-15 SOE 只读遍历(count/read);写侧仍归 protectTask/defaultTask */
#include "ui_tokens.h" /* UI 设计令牌:循环周期 UI_TASK_TICK_MS / 待机阈值 UI_STANDBY_IDLE_MS */
#include "sys_clock.h"
#include "prot_elm.h" /* 道F task-3 装置维护:强制复归请求 + 动作态元件位图 */
#include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */
#include "net_latency_test.h" /* M3 探针:ui_loop(生产循环实测周期) */
#include "task_watchdog.h"
#include "task_prof.h" /* P2 常开探针:循环周期 / 工作耗时 / 推流往返 */
#include "sb_link.h" /* 屏板链路:帧 B 组帧 / 按键出队 / 统计与离线判定(批 2) */
/*
* 函数功能:待机屏底部时钟提供回调(平台差异:MCU = sys_clock_get_str)
* 入口参数:buf - 时间串输出缓冲 char *
* len - buf 容量 size_t
* 返回值:无
*/
static void ui_task_time_cb(char *buf, size_t len) {
if (sys_clock_get_str(buf, len) == NULL && len > 0u) {
buf[0] = '\0'; /* 墙钟无效:空串(待机屏时钟行留白) */
}
}
/*
* UI 记录源编号 → syslog_id_t 映射表(道F task-6 起 7 类,task-3 装置维护「清除记录」
* 要在 7 行上分行显示;表长 = UI_W_REC_LOG_N,两端映射表同源同长)
*
* ⚠ 顺序契约(Lead 裁定 2026-10-26):UI_W_REC_LOG_* 逐字等于 syslog_id_t 枚举序,
* 只准尾部追加——枚举序就是 App/log 两端 s_paths[]/s_caps[] 的下标,中间插值会
* 安静地把事故读成告警。下面的编译期守卫就是这条契约在本端的机械防线(记录侧
* 已有 TC-SYSLOG-130/131/131b,UI 侧对应的一条在 test/host/ui_data TC-UIDATA-024,
* 两端各钉一次,谁中间插号谁编译不过 / 单测红)。
* 即便如此,适配层**始终显式逐项映射**、从不把 UI 编号强转成 syslog_id_t:
* 等值是今天的事实,不是页面可以依赖的契约(UI_W_REC_LOG_* 对页面仍是不透明号)。
*/
static const syslog_id_t s_log_map[UI_W_REC_LOG_N] = {
SYSLOG_RUN, SYSLOG_SELFTEST, SYSLOG_ACCIDENT, SYSLOG_ALARM,
SYSLOG_START, SYSLOG_OPER, SYSLOG_YK
};
#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)
typedef char ui_task_log_order_check_t[
((UI_W_REC_LOG_RUN == (uint8_t)SYSLOG_RUN) &&
(UI_W_REC_LOG_SELFTEST == (uint8_t)SYSLOG_SELFTEST) &&
(UI_W_REC_LOG_ACCIDENT == (uint8_t)SYSLOG_ACCIDENT) &&
(UI_W_REC_LOG_ALARM == (uint8_t)SYSLOG_ALARM) &&
(UI_W_REC_LOG_START == (uint8_t)SYSLOG_START) &&
(UI_W_REC_LOG_OPER == (uint8_t)SYSLOG_OPER) &&
(UI_W_REC_LOG_YK == (uint8_t)SYSLOG_YK) &&
(UI_W_REC_LOG_N == (uint8_t)SYSLOG_COUNT)) ? 1 : -1];
#endif
/*
* 函数功能:记录 reader 适配——UI 不透明编号 → syslog_id_t 后取条数
* 入口参数:log_id - UI 记录源编号(UI_W_REC_LOG_*) uint8_t
* 返回值:记录条数;未知编号返回 0(页面退化为空态,与合并前直调同形) uint16_t
* 限定条件:uiTask 上下文同步调用(记录页与清除记录页的绘制路径,无 I/O 之外开销:
* syslog_count 读的是文件长度/内存计数,不逐条读盘)
*/
static uint16_t ui_task_rec_count(uint8_t log_id) {
if (log_id >= (uint8_t)UI_W_REC_LOG_N) {
return 0u;
}
return (uint16_t)syslog_count(s_log_map[log_id]);
}
/*
* 函数功能:记录 reader 适配——读记录字段直通 sys_log(错误码不改写)
* 入口参数:log_id/idx/ts/ts_len/desc/desc_len - 契约见 ui_w_records.h ui_rec_reader_t
* 返回值:0 成功;-1 编号/序号越界(未知编号同 -1);-2 读失败 int
* 限定条件:同上
*/
static int ui_task_rec_get(uint8_t log_id, uint32_t idx, char *ts, uint32_t ts_len,
char *desc, uint32_t desc_len) {
if (log_id >= (uint8_t)UI_W_REC_LOG_N) {
return -1;
}
return syslog_get_record(s_log_map[log_id], idx, ts, (size_t)ts_len, desc,
(size_t)desc_len);
}
/*
* ============================ 道F task-3:装置维护写通道(UI 注入执行侧)
*
* 为什么在这一层实现而不是在页面里:页面属 Drivers/BSP/UI,反调 App 侧的
* sys_clock / prot_elm / sys_log 是分层方向的反例(layer-deps 拦),更关键的是
* **模拟器根本不链 sys_clock**(simulator/dbg_log.h:4「模拟器无 sys_clock/RTC」)——
* 页面里用一次就是"MCU 编得过、模拟器链接期爆"。写动作一律走 ui_data_maint_ops_t,
* 两端各给实现(口径 docs/UI设计/UI_数据供给设计说明.md §6.2/§7.2)。
*
* uiTask 阻塞上界(IWDG 800ms 窗、本任务在 required_mask 内):本通道里只有
* 时钟设置会碰硬件——sys_clock_set_unix 内含 1 次 SD2506 写,再叠加本适配层刻意
* 加的回读复核 1 次读,各 SD2506_I2C_TIMEOUT_MS=100ms ⇒ 最坏 ≈200ms 落在一次
* "保存"按键上(心跳在循环尾,仍远小于 800ms;对时任务同样在本机上做过这件事)。
* 复归/清除两条只写内存标志或投递给别的任务,无 I/O。
*/
/*
* 函数功能:设置墙钟并**回读复核**(真机 = 写 RAM 基准 + 写 SD2506 RTC)
* 入口参数:a_epoch - Unix 秒(本地时间域,与 sys_clock 同一口径,无时区偏移) uint32_t
* 返回值:0 - 已生效且 RTC 回读一致;-1 - 越界 / 未生效 / RTC 回读不符 int
* 限定条件:uiTask 上下文(页面"保存"按键直达,同步返回终态)
* 函数说明:⚠ **sys_clock_set() 恒返 0**(App/util/sys_clock.c:462 起:RTC 写失败
* 只打 DBG_ERROR,不改返回值),所以本函数不能拿它的返回值当结论——
* 回读是唯一真判据;而只回读 sys_clock_get() 证明的仅是"RAM 基准换了",
* 面板对时的用户意图是**掉电后还要是对的**,故必须回读 RTC 本身。
* RTC 坏了但墙钟已改时本函数返 -1(屏上"保存行=ERR + 值列跟着新时间",
* 两个事实各说各的,比笼统报 OK 诚实);秒位允许 ±3 与"分/时/日回绕"两种
* 形态,因为两次读之间墙钟与 RTC 都在走。
* 上限 UI_DATA_CLOCK_EPOCH_MAX(2038) 在 UI 层已挡,这里再挡一次:
* 执行侧不信任调用侧(0 还兼作"未建钟"哨兵,面板永远不该发 0)
*/
static int ui_task_maint_clock_set(uint32_t a_epoch) {
sd2506_time_t want;
sd2506_time_t got;
uint32_t now;
int ds;
if ((a_epoch == 0u) || (a_epoch > (uint32_t)UI_DATA_CLOCK_EPOCH_MAX)) {
return -1;
}
if (sys_clock_set_unix(a_epoch) != 0) {
return -1; /* 今天到不了这条腿(sys_clock_set 恒 0),留着是因为它是契约 */
}
now = sys_clock_get();
if ((now == UINT32_MAX) || ((now + 2u) < a_epoch) || (now > (a_epoch + 2u))) {
return -1; /* RAM 基准没跟上:连"现在就对"都没成立 */
}
if (sys_clock_get_time(&want) != 0) {
return -1;
}
if (sd2506_get_time(&got) != BSP_OK) {
return -1; /* RTC 读不动:掉电保持这件事无法证明 */
}
if ((got.year != want.year) || (got.month != want.month) || (got.day != want.day) ||
(got.hour != want.hour) || (got.minute != want.minute)) {
return -1;
}
ds = (int)got.second - (int)want.second;
if (ds < 0) {
ds = -ds;
}
if ((ds > 3) &&
!(((want.second >= 58u) && (got.second <= 3u)) ||
((got.second >= 58u) && (want.second <= 3u)))) {
return -1;
}
return 0;
}
/*
* 函数功能:请求强制复归(跨上下文:protectTask 下一拍消费)
* 入口参数:无
* 返回值:0 - 已投递(prot_elm_request_reset 无拒绝分支) int
* 限定条件:uiTask 上下文;不等待执行结果
* 函数说明:只投递不阻塞(契约:UI 页面二次确认后调,页面立刻给"已请求复归")。
* **不在这里投操作记录**:远端 104 YK11 那条已经在
* App/protocol/iec104/iec104_pts.c 投过,在这里再投一次就会给每一次
* 主站复归多记一条"面板复归",审计面直接失真。本地这条由页面投
* (只有页面知道"这是人按的"),措辞也不同串:见 Drivers/BSP/UI/ui_page_reset.c
*/
static int ui_task_maint_reset_req(void) {
prot_elm_request_reset();
return 0;
}
/*
* 函数功能:取当前处于动作态的保护元件个数(复归页观测行 "保护动作 N" 的 N)
* 入口参数:无
* 返回值:位图中置 1 的位数(纯 RAM 读,无锁) uint32_t
* 限定条件:uiTask 绘制路径每帧可直调
* 函数说明:给个数而不是位图,是为了让 160x160 上一行放得下、也让"复归前 N>0 →
* 复归后 N=0"这个台架判据肉眼可读;判据前提(产品构建处于未标定闭锁,
* App/task/protect_task.c:98 停走判据 ⇒ 闭锁期 N 恒 0)已写进页面注释
*/
static uint32_t ui_task_maint_active_count(void) {
uint32_t mask = prot_elm_active_mask();
uint32_t n = 0u;
while (mask != 0u) {
n += (mask & 0x1u);
mask >>= 1;
}
return n;
}
/*
* 函数功能:投一条本地操作记录(面板动作留痕)
* 入口参数:a_desc - 描述串(ASCII,长度须 <= SYSLOG_DESC_MAX=28) const char *
* 返回值:无
* 限定条件:uiTask 上下文;只在动作真正被受理之后由页面调用
* 函数说明:用 quiet 口(与 iec104_serv.c 对时留痕同一选择):uiTask 不属于深链
* 任务,但 quiet 口的收/丢判据与 syslog_post 一致且不在调用链上摊开
* 行缓冲,留痕这件事没理由选更贵的那个口。
* 丢条不回报页面:syslog 侧有累计丢弃计数(syslog_post_quiet_dropped())
* 可查;页面上"动作做了没"与"这条审计写没写"是两件事,不该混在一个
* 态标里——把后者做成 ERR 会让人以为复归没做而再按一次
*/
static void ui_task_maint_oper_note(const char *a_desc) {
if (a_desc == NULL) {
return;
}
syslog_post_quiet(SYSLOG_OPER, a_desc);
}
/*
* 函数功能:请求清除某一类记录(只投递,落地在 defaultTask 的 syslog_drain 尾部)
* 入口参数:a_log_id - UI 记录源编号(UI_W_REC_LOG_*) uint8_t
* 返回值:0 - 已受理(MCU 端随后通常是 SYSLOG_CLEAR_WAIT);-1 - 越界编号 int
* 限定条件:uiTask 上下文
* 函数说明:本层是 UI 编号 → syslog_id_t 的**唯一**映射点之一(与 s_log_map 同表),
* 绝不在 UI 侧做 f_unlink / 也不调 MCU 专属的 syslog_clear_apply():
* 记录卷的属主是 defaultTask,uiTask 去碰 FAT 既违反唯一写者,又给
* IWDG required_mask 内的任务添一条无上界的存储路径(口径 App/log/README.md)
*/
static int ui_task_maint_clear_req(uint8_t a_log_id) {
if (a_log_id >= (uint8_t)UI_W_REC_LOG_N) {
return -1;
}
return (syslog_request_clear(s_log_map[a_log_id]) == 0) ? 0 : -1;
}
/*
* 函数功能:取上条清除请求的状态(值直通,不改写、不翻译)
* 入口参数:a_log_id - UI 记录源编号 uint8_t
* 返回值:UI_DATA_CLEAR_ST_NONE/_OK/_WAIT/_FAIL;越界编号按 NONE int
* 限定条件:uiTask 绘制路径每帧可直调(纯内存状态读)
* 函数说明:SYSLOG_CLEAR_* 与 UI_DATA_CLEAR_ST_* 逐值相等(0/1/-1/-2),由
* test/host/ui_data TC-UIDATA-024 钉住;这里保持直通,是为了让页面
* 显示的就是日志侧的事实,而不是"适配层以为的事实"
*/
static int ui_task_maint_clear_state(uint8_t a_log_id) {
if (a_log_id >= (uint8_t)UI_W_REC_LOG_N) {
return UI_DATA_CLEAR_ST_NONE;
}
return syslog_clear_state(s_log_map[a_log_id]);
}
/* 维护通道函数表(六件齐全;ui_data_maint_bind 拒绝残表,缺件就是装配错) */
static const ui_data_maint_ops_t s_maint_ops = {
ui_task_maint_clock_set,
ui_task_maint_reset_req,
ui_task_maint_active_count,
ui_task_maint_oper_note,
ui_task_maint_clear_req,
ui_task_maint_clear_state
};
static const ui_rec_reader_t s_rec_reader = { ui_task_rec_count, ui_task_rec_get };
/*
* 函数功能:SOE 记录条数适配(道E task-15 reader 注入)——读落盘文件的完整槽数
* 入口参数:无
* 返回值:条数;超过 uint16 上限时钳到 65535(组件契约是 uint16_t) uint16_t
* 限定条件:uiTask 上下文同步调用(SOE 记录页绘制路径)
*/
static uint16_t ui_task_soe_count(void) {
uint32_t n = soe_store_count();
return (n > 65535u) ? 65535u : (uint16_t)n;
}
/*
* 函数功能:读第 idx 条 SOE 记录并翻成 UI 视图(二进制 → 不透明显示字段)
* 入口参数:idx - 记录序号 uint32_t
* p_out - UI 侧记录视图(契约见 Drivers/BSP/UI/ui_w_soe.h) ui_w_soe_rec_t *
* 返回值:0 - 成功;-1 - 读失败 / 卷忙 / 校验和不过(页面这行退化显示,不猜) int
* 限定条件:uiTask 绘制路径;soe_store_read() 是 try 锁非阻塞,绝不拖住 UI 节拍
* 函数说明:时标**取记录内的 CP56**(事件自己的时间),不用"现在"换算。两条口径
* 叠起来正好与文本记录页一致,别"顺手补 8 小时":
* ① sys_clock 的 epoch 域本身就是本地时间(App/util/sys_clock.h 的
* sys_clock_set_unix 注明"Unix epoch time (秒,本地时间)");
* ② App/log/soe_rec.c 的 epoch→CP56 分解不加任何时区偏移。
* 所以这里打出来的墙上时钟 == 运行/自检记录页里 sys_clock_get_str() 的口径;
* 哪天真要引入显式时区,两侧必须一起改(跨模块陷阱,注释即防线)。
* 时钟无效(IV)不写在这里:它是显示语义,由 UI 侧按 SOE_FLG_IV 决定
* (组件把那行名字打"--"、值打"时钟无效"),App 只交代原始记录。
*/
static int ui_task_soe_get(uint32_t idx, ui_w_soe_rec_t *p_out) {
soe_rec_t rec;
uint32_t sec;
if ((p_out == NULL) || (soe_store_read(idx, &rec) != 0)) {
return -1;
}
/* CP56 字节表见 App/log/soe_rec.c 的 s_soe_epoch_to_cp56:
* [0..1]=分内毫秒(秒域时基 → 恒为秒×1000,亚秒不造假)[2]=分 [3]=时
* [4]=日 [5]=月 [6]=年-2000 */
sec = (((uint32_t)rec.cp56[1] << 8) | (uint32_t)rec.cp56[0]) / 1000u;
(void)snprintf(p_out->ts, sizeof(p_out->ts), "%04u-%02u-%02u %02u:%02u:%02u",
(unsigned)(2000u + ((uint32_t)rec.cp56[6] & 0x7Fu)),
(unsigned)((uint32_t)rec.cp56[5] & 0x0Fu),
(unsigned)((uint32_t)rec.cp56[4] & 0x1Fu),
(unsigned)((uint32_t)rec.cp56[3] & 0x1Fu),
(unsigned)((uint32_t)rec.cp56[2] & 0x3Fu),
(unsigned)sec);
p_out->type = rec.type;
p_out->elm = rec.elm_id;
p_out->flags = rec.flags;
p_out->seq = rec.seq;
p_out->value = rec.value;
return 0;
}
static const ui_soe_reader_t s_soe_reader = { ui_task_soe_count, ui_task_soe_get };
/*
* 函数功能:推流往返边界的心跳续命钩(F3,注册给 ui_net_sink)
* 入口参数:无
* 返回值:无
* 限定条件:uiTask 轮询上下文(sink 在往返返回处调用),只做置位、不阻塞
* 函数说明:为何经回调注入而非 sink 直调 task_wd_beat()——分层边界(BSP 不得
* include App 头,layer-deps 门禁);心跳属主仍是本任务。
*/
static void ui_task_sink_beat(void) {
task_wd_beat(TASK_WD_UI);
}
#define DBG_TAG "[UI_TASK]"
#include "dbg_log.h"
/*
* 函数功能:等待网络就绪事件
* 入口参数:无
* 返回值:无
* 限定条件:sys_evtHandle 为 NULL 时直接返回
* 函数说明:无限期等待 SYS_EVT_NET_READY 标志
*/
static void ui_task_wait_net_ready(void) {
if (sys_evtHandle != NULL) {
/* osFlagsNoClear:就绪位是状态,不能被本任务消费掉(否则其它等待者永不唤醒)。
* 2026-10-02 台架修复:等待改 100ms 分片 + 每片喂心跳(与 timeSync/netTask/
* ftpTask 入口同模式)——IWDG 必需掩码含 UI 位,原 osWaitForever 无心跳:
* IWDG 启动(存储就绪)时网络未必就绪 → 掩码 800ms 不可达 → 停喂 → 复位循环。
* 100ms 分片 = 抽样窗,纯任务睡眠零队列开销。 */
for (;;) {
uint32_t r = osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
osFlagsWaitAny | osFlagsNoClear, 100u);
if ((r & osFlagsError) == 0u) {
break; /* NET_READY 已置位(状态位,未被消费) */
}
task_wd_beat(TASK_WD_UI); /* 看门狗:分片喂心跳(屏障等待态) */
}
}
}
/*
* 函数功能:把屏板键号映射为 UI 键码
* 入口参数:board_key - 屏板帧 A 的 key 字节(1~10,语义见 docs/任务规划/屏板链路.md §4.3)
* 返回值:UI_KEY_* 之一;0 = 不映射(无键 / RESET / FACTORY / 屏板无物理入口的 5、6)
* 限定条件:只在 uiTask 上下文调用(ui_port_queue_key 的契约是输入子系统已初始化)
* 函数说明:1. 屏板 key_configs[] 只有 **7 项**,可达值仅 1/2/3/4/7/8/9——别按屏板 readme
* 的 8 键表实现(5/6 = ADD/DEC 在实物上没有入口,永远收不到)。
* 2. 9(RESET)/10(FACTORY)属决策四待裁定项:**先丢弃并留痕**,不要擅自
* 映射成参考机的 F1 / F1+ENT——否则现场按一下"复位"会把菜单弹到未知页面,
* 而且那种误映射极难从现象反推回映射表。
*/
static uint8_t ui_task_board_key_map(uint8_t board_key) {
switch (board_key) {
case 1u: return UI_KEY_ENTER;
case 2u: return UI_KEY_UP;
case 3u: return UI_KEY_DOWN;
case 4u: return UI_KEY_ESC;
case 7u: return UI_KEY_LEFT;
case 8u: return UI_KEY_RIGHT;
default: return 0u;
}
}
/* 组帧失败只留痕一次:组帧每 10 ms 一次,每拍打一行会把控制台冲爆(120 s 就 1.2 万行),
* 而 BSP_ERR_PARAM/BSP_ERR_STATE 是配置类错误,出现一次就是一直出现,一行足够定位。 */
static uint8_t s_sb_frame_err_logged;
/*
* 函数功能:UI 主任务入口,初始化显示/菜单/推流并进入刷新主循环
* 入口参数:argument - FreeRTOS 任务参数(未使用) void *
* 返回值:无
* 限定条件:FreeRTOS 已启动
* 函数说明:1. 等待 SYS_EVT_NET_READY 后依次初始化 ui_port/ui_menu/ui_net_sink
* 2. 主循环周期调用 ui_port_step 与 ui_net_sink_poll 刷新界面
*/
void ui_task_func(void *argument) {
(void)argument;
task_wd_bind(TASK_WD_UI); /* FTL 心跳路由:登记本任务句柄(入口一次) */
/* 等待网络就绪期间不可能喂狗:本任务此时尚未进入主循环,而就绪位由 netTask 置位。
* 该段若长期不成立,看门狗会复位——这是期望行为(网络 bring-up 卡死须暴露)。
* 注意本任务在 required_mask 内,主循环跑起来后须保证每 100ms 窗口内至少喂一次。 */
DBG_INFO("UI task: started, wait net ready");
ui_task_wait_net_ready();
DBG_INFO("UI task: net ready, init port");
DBG_INFO("UI task: ui_port_init enter");
if (ui_port_init() != 0) {
DBG_ERROR("UI port init: FAIL");
for (;;) { osDelay(1000); }
}
DBG_INFO("UI port init: OK");
/* 记录 reader 注入:必须在 ui_menu_init 之前(记录页在此前不可能被打开,
* 但契约把"启动期先绑定"写死;未绑定窗口组件按空态防御——见 ui_w_records.h) */
ui_w_records_bind(&s_rec_reader);
ui_w_soe_bind(&s_soe_reader); /* task-15:SOE 记录页数据入口(未绑定按"无记录"空态) */
/* 装置维护写通道注入(道F task-3:时钟设置 / 强制复归 / 清除记录):同样必须先于
* ui_menu_init。未注入的窗口里六个入口一律安全返回(页面显 ERR 或空态),
* 既不崩也不假成功;但残表是真故障,必须能在启动日志里看见,不能静默 */
if (ui_data_maint_bind(&s_maint_ops) != 0) {
DBG_ERROR("UI maint bind: FAIL");
}
/* 快照层复位:必须先于 ui_menu_init(首帧之前任何页面被打开都按"无数据"
* 渲染 "--",不会读到上电残留)——UI_数据供给设计说明.md §5.3 */
ui_data_init();
/* 首帧立即发布:待机屏/菜单屏在启动早期就可能被远端推流打开,等 200 ms
* 会闪一下 "--"(同文档 §5.3);本调用在 ui_menu_init 之前,页面尚未建立,
* 发布路径不触 LVGL,安全 */
ui_data_feed_tick();
DBG_INFO("UI task: ui_menu_init enter");
if (ui_menu_init() != 0) {
DBG_ERROR("UI menu init: FAIL");
for (;;) { osDelay(1000); }
}
DBG_INFO("UI menu init: OK");
if (ui_standby_init(UI_STANDBY_IDLE_MS, ui_task_time_cb) != 0) {
DBG_ERROR("UI standby init: FAIL");
} else {
DBG_INFO("UI standby: ready (idle=60s)");
}
if (ui_net_sink_start(UI_NET_SINK_PORT) != 0) {
DBG_ERROR("UI net sink: start FAIL");
} else {
DBG_INFO("UI net sink: started (port %u)", (unsigned)UI_NET_SINK_PORT);
}
/* F3:往返边界心跳钩(契约见 ui_net_sink.h;须在首轮 poll 之前注册)。
* start 失败也要注:失败路径的 close/重建重试同样经往返 */
ui_net_sink_set_beat_cb(ui_task_sink_beat);
#if defined(TEST_LATENCY_PROBE)
/* M3 探针 ui_loop:主循环实测周期(= LVGL 渲染 + 推流轮询 + osDelay(10))。
* 每轮开头先结算"上一轮到此轮"的间隔,再重置标记 → 得到真实循环周期。
* 单写者(本任务)/单读者(testTask),样本经 lat_probe_add 落静态缓冲。 */
lat_tmark_t tm_loop;
uint8_t tm_loop_valid = 0U;
#endif
/* 常开探针:TEST_LATENCY_PROBE 只在 CH395F 套件构建下打开,
* 且样本消费者是 testTask(产品构建里不跑)→ 产品构建拿不到任何 UI 周期数据。
* 故此处独立埋点,恒定生效、只写本模块的静态计数,由 defaultTask 周期上报。 */
while (1) {
/* 循环周期用 enter/exit 测:exit 结算"上轮起点 → 本轮起点",enter 记本轮起点。
* 首轮 exit 时尚未 enter 过 → 内部直接返回,不会拿 0 当起点。
* 选 enter/exit 而非"自己存一个 prev"的额外收益:两次循环之间段内标志保持为 1
* → 报告可读出"本轮已进行多久"。**这正是'uiTask 是否被饿住'的直接证据**:
* 若该值在两次报告间持续增长而 uic 不动,就是被调度饿死/阻塞,而非渲染拖长。 */
task_prof_exit(TASK_PROF_UI_PERIOD);
task_prof_enter(TASK_PROF_UI_PERIOD);
const uint32_t prof_t0 = task_prof_cyc(); /* 单轮工作耗时基准 */
#if defined(TEST_LATENCY_PROBE)
if (tm_loop_valid != 0U) {
lat_probe_add_seg("ui_loop", lat_mark_us(&tm_loop));
}
lat_mark(&tm_loop);
tm_loop_valid = 1U;
#endif
/* 数据装配节拍(200 ms 内部自分频,未到期时只比一次 tick):
* 排在渲染之前,本轮绘制的就是本轮刚发布的那一帧
* (UI_数据供给设计说明.md §5.1 单写者同上下文的前提就在这儿) */
ui_data_feed_tick();
ui_port_step(UI_TASK_TICK_MS);
/* ---- 屏板链路(批 2):帧 B 组帧 + 按键上行,全在任务上下文 ----
* 组帧紧跟渲染:本轮画出来的就是本轮组进去的那一帧。
* 排在推流往返**之前**,是因为那个往返最坏能等 ≈4 s(LCDMEM 应答 3204 B),
* 排在它后面会让屏板画面跟着网络一起卡(同一份显存、两个出口,出口不能互相拖)。
* 显存直接取活帧缓冲:不留 3200 B 影子副本,sb_link 内部与帧里的点阵段 memcmp。
* LED 掩码暂传 0(批 3 才落状态灯映射表),故 info[0..3] 现恒为 0。 */
{
bsp_err_t sb_ret = sb_link_frame_update(ui_port_get_framebuffer(), 0u);
uint8_t board_key;
/* BSP_ERR_BUSY 是常态(上一帧还在 59.6 ms 的发送窗口里),不必留痕;
* 其余失败打一行即可(见 s_sb_frame_err_logged 的说明)。 */
if ((sb_ret != BSP_OK) && (sb_ret != BSP_ERR_BUSY) &&
(s_sb_frame_err_logged == 0u)) {
s_sb_frame_err_logged = 1u;
DBG_ERROR("screenlink frame update ret=%d", sb_ret);
}
/* 逐键必到:一次取一个,直到队列空。9/10 不映射(决策四待定),丢弃并留痕。
* 走 ui_port_queue_key 而不是 ui_net_sink_inject_key:后者只是 7003 后端
* 对同一个函数的转发(见 ui_net_sink_backend_mcu.c),多绕一层还得让
* App 去 include 推流后端头,没必要。两条入口最终进的是同一个输入队列。 */
while ((board_key = sb_link_key_take()) != 0u) {
uint8_t ui_key = ui_task_board_key_map(board_key);
if (ui_key != 0u) {
ui_port_queue_key(ui_key);
} else {
DBG_INFO("board key %u unmapped (RESET/FACTORY pending decision)",
(unsigned)board_key);
}
}
}
/* 心跳**先于**推流往返发出(判读口径 docs/子系统设计/看门狗设计说明.md §11.3):UI 心跳测的是
* "UI 自身被调度到了",而非"netTask 回执快慢"。原写法心跳在 sink 之后,
* 致实测 uiw≈uis≈50ms(心跳被 netTask 往返主导)。提到往返之前后,
* 即使往返偶发变慢,心跳也不被其拖垮。
* ⚠ 若往返**永久**阻塞(xTaskNotifyWait portMAX_DELAY 不返回),本改只能发
* 一次心跳,救不了 → 彻底解耦须走事件驱动 accept,见文件头修改记录 v1.3。
* v1.4 补充:sink 内每个往返**返回点**已各自补喂(set_beat_cb 注入),
* 「单轮多个慢往返之和」不再构成缺席链;剩单往返 >800ms 才报——那是
* netTask 自身停摆,按位判据抓对了人(上界归「net 层跨上下文收口」)。 */
task_wd_beat(TASK_WD_UI);
/* 推流轮询段单独 enter/exit:它内部经 net_accept → net_wait_result 走到
* xTaskNotifyWait(portMAX_DELAY) 的**无限期阻塞**。若 uiTask 永久卡在这里,
* 样本永不结算,只有 task_prof 的"仍在段内已 X us"能证明它卡在何处 */
task_prof_enter(TASK_PROF_UI_SINK);
ui_net_sink_poll();
task_prof_exit(TASK_PROF_UI_SINK);
/* 屏板链路统计上报与离线判定(内部自分频,5 s 一次、只在计数有变化时打一行)。
* 放在推流段之后:它只是读一份快照 + 偶尔一行日志,不进推流往返的耗时统计。
* 组帧与注键在渲染之后就做完了(见上),这里只剩"看一眼"——离线判定要求每拍都看,
* 所以它不跟着 5 s 的上报周期走。 */
sb_link_poll(osKernelGetTickCount());
/* 单轮工作耗时(不含 osDelay):与循环周期对比即得"调度延迟" */
task_prof_sample(TASK_PROF_UI_WORK, task_prof_cyc() - prof_t0);
osDelay(UI_TASK_TICK_MS);
}
}